From 3b2e98838f7ade9f7421b265f5f14709a086d92a Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 29 Nov 2024 17:28:57 -0800 Subject: [PATCH 01/34] Tool packaging abstractions. Needs tests. --- agentstack/generation/tool_generation.py | 10 +- agentstack/packaging.py | 132 +++++++++++++++++- agentstack/tools/agent_connect/README.md | 23 +++ .../agent_connect/common/__init__.py} | 37 ----- .../config.json} | 1 - .../tools/agent_connect/crew/__init__.py | 41 ++++++ agentstack/tools/agent_connect/pyproject.toml | 16 +++ pyproject.toml | 1 + tests/fixtures/tool_config_max.json | 14 ++ tests/fixtures/tool_config_min.json | 5 + 10 files changed, 232 insertions(+), 48 deletions(-) create mode 100644 agentstack/tools/agent_connect/README.md rename agentstack/{templates/crewai/tools/agent_connect_tool.py => tools/agent_connect/common/__init__.py} (61%) rename agentstack/tools/{agent_connect.json => agent_connect/config.json} (85%) create mode 100644 agentstack/tools/agent_connect/crew/__init__.py create mode 100644 agentstack/tools/agent_connect/pyproject.toml create mode 100644 tests/fixtures/tool_config_max.json create mode 100644 tests/fixtures/tool_config_min.json diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 4e4c5463..7056bbb9 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -41,7 +41,6 @@ class ToolConfig(BaseModel): tools_bundled: bool = False cta: Optional[str] = None env: Optional[str] = None - packages: Optional[List[str]] = None post_install: Optional[str] = None post_remove: Optional[str] = None @@ -64,6 +63,10 @@ def from_json(cls, path: Path) -> 'ToolConfig': def get_import_statement(self) -> str: return f"from .{self.name}_tool import {', '.join(self.tools)}" + def get_path(self) -> Path: + return get_package_path()/'tools'/tool_name + + def add_tool(tool_name: str, path: Optional[str] = None): if path: path = path.endswith('/') and path or path + '/' @@ -79,9 +82,8 @@ def add_tool(tool_name: str, path: Optional[str] = None): tool_data = ToolConfig.from_tool_name(tool_name) tool_file_path = importlib.resources.files(f'agentstack.templates.{framework}.tools') / f'{tool_name}_tool.py' - 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 + + packaging.install_tool(tool_data, path) 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 if tool_data.env: # if the env vars aren't in the .env files, add them diff --git a/agentstack/packaging.py b/agentstack/packaging.py index 86eaefb1..0eeecb01 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -1,13 +1,133 @@ +""" +packaging +========= +Packaging, in our context, is management of tools and dependencies inside an +agentstack project. This module provides functions to install, remove, and +upgrade tools and install dependencies. +""" import os, sys +import shutil +from typing import TYPE_CHECKING, Optional +from packaging.metadata import Metadata +from agentstack.utils import term_color, get_package_path, get_framework +if TYPE_CHECKING: # TODO move ToolConfig to a separate file + from agentstack.generation.tool_generation import ToolConfig -PACKAGING_CMD = "poetry" +class PackageManager: + cmd: str + install: str + update: str + + def __init__(self, cmd: str, install: str, update: str): + self.cmd, self.install, self.update = cmd, install, update + + def install(self, *args): + os.system(f"{self.cmd} {self.install} {' '.join(args)}") + + def upgrade(self, *args): + os.system(f"{self.cmd} {self.update} {' '.join(args)}") -def install(package: str): - os.system(f"{PACKAGING_CMD} add {package}") + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = None): + """ + Copy a tool's source files from the agentstack install directory to the app + tools directory and install relevant dependencies. + + `tool` is a ToolConfig object representing the tool to be installed. + `framework` is the framework of the app which will be used to select the + relevant `opional-dependencies`. + `path` is the path to the app directory. If `None`, the current working directory. + """ + raise NotImplementedError + + def remove_tool(self, tool: 'ToolConfig', path: Optional[str] = None): + """ + Remove a tool's source files from the app tools directory. + + `tool` is a ToolConfig object representing the tool to be removed. + `path` is the path to the app directory. If `None`, the current working directory. + """ + shutil.rmtree(tool.get_path()) + +class UV(PackageManager): + def __init__(self): + super().__init__('uv', 'add', 'pip install --upgrade') + + def install_tool(self, tool: 'ToolConfig', path: Optional[str] = None): + # uv pip install --directory --target --link-mode copy --editable .[] + os.system(f"{self.cmd} pip install --link-mode copy --directory {tool.get_path()} --target {path}src/tools/ --editable .[{framework}]") + +class Poetry(PackageManager): + def __init__(self): + super().__init__('poetry', 'add', 'update') + + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = None): + # poetry doesn't have a way to install into a target directory afaik so we + # first install the dependencies and then manually copy the tool files + # poetry install --no-root --directory --with + arg = f' --directory {package_path}' + if optional_dependencies_tag: + arg += f" --with {','.join(optional_dependencies_tags)}" + os.system(f"{self.cmd} install --no-root {arg}") + shutil.copytree(tool.get_path(), f"{path}src/tools/{tool.name}") + +class PIP(PackageManager): + def __init__(self): + super().__init__('pip', 'install', 'install --upgrade') + + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = None): + # pip install --target --editable .[] + os.system(f"{self.cmd} {self.install} --target {path}src/tools/ --editable {tool.get_path()}[{framework}]") + +UV = UV() +POETRY = Poetry() +PIP = PIP() + +PACKAGE_MAGANAGERS = [UV, POETRY, PIP] # in order of preference -def remove(package: str): - os.system(f"{PACKAGING_CMD} remove {package}") +def detect_package_manager() -> PackageManager: + # TODO use package manager specified in agentstack.json + # TODO what about `path` + # if ConfigFile(path).package_manager: + # for manager in PACKAGE_MANAGERS: + # if manager.cmd == ConfigFile.package_manager: + # return manager + + # detect best installed package manager + for manager in PACKAGE_MANAGERS: + if shutil.which(manager.cmd): + return manager + + print(term_color('No package manager detected', 'red')) + print('Install one of the following package managers: ', end='') + print(', '.join(manager.cmd for manager in PACKAGE_MANAGERS)) + sys.exit(1) + +def install(package: str): + detect_package_manager().install(package) def upgrade(package: str): - os.system(f"{PACKAGING_CMD} update {package}") + detect_package_manager().upgrade(package) + +def get_tool_metadata(tool: 'ToolConfig') -> Metadata: + """ + Parsed pyproject.toml for the tool + NOTE Does not include `dependencies` or `optional-dependencies` + """ + return Metadata.from_pyproject(get_tool_path()/'pyproject.toml') + +def install_tool(tool: 'ToolConfig', path: Optional[str] = None): + """ + Copy a tool's applicable source files and install relevant dependencies + """ + framework = get_framework(path) + manager = detect_package_manager() + manager.install_tool(tool, framework, path) + +def remove_tool(tool: 'ToolConfig', path: Optional[str] = None): + """ + Remove a tool's source files. + Removing dependencies is messy, so we just leave them installed. + """ + manager = detect_package_manager() + manager.remove_tool(tool, path) diff --git a/agentstack/tools/agent_connect/README.md b/agentstack/tools/agent_connect/README.md new file mode 100644 index 00000000..815fa2d3 --- /dev/null +++ b/agentstack/tools/agent_connect/README.md @@ -0,0 +1,23 @@ + + +Tool Implementation +=================== + +`config.json` +------------- +This contains the configuration for the tool for use by agentstack. + +`pyproject.toml` +---------------- +This contains the build configuration for the tool. + +`crew` +------ +Python package which contains the tool implementation specific to CrewAI. +Use relative imports to include shared code from the root of the tool's package. +Deploy/install command should handle both single files and directories. + +Additional frameworks will get their own directories. + + + diff --git a/agentstack/templates/crewai/tools/agent_connect_tool.py b/agentstack/tools/agent_connect/common/__init__.py similarity index 61% rename from agentstack/templates/crewai/tools/agent_connect_tool.py rename to agentstack/tools/agent_connect/common/__init__.py index f90a49c6..1dfd7e1c 100644 --- a/agentstack/templates/crewai/tools/agent_connect_tool.py +++ b/agentstack/tools/agent_connect/common/__init__.py @@ -1,4 +1,3 @@ -from crewai_tools import tool from dotenv import load_dotenv import os @@ -57,39 +56,3 @@ def generate_did_info(node: SimpleNode, did_document_path: str) -> None: generate_did_info(agent_connect_simple_node, did_document_path) agent_connect_simple_node.run() -@tool("Send Message to Agent by DID") -async def send_message(message: str, destination_did: str) -> bool: - """ - Send a message through agent-connect node. - - Args: - message: Message content to be sent - destination_did: DID of the recipient agent - Returns: - bool: True if message was sent successfully, False otherwise - """ - try: - await agent_connect_simple_node.send_message(message, destination_did) - print(f"Successfully sent message: {message}") - return True - except Exception as e: - print(f"Failed to send message: {e}") - return False - -@tool("Receive Message from Agent") -async def receive_message() -> tuple[str, str]: - """ - Receive message from agent-connect node. - - Returns: - tuple[str, str]: Sender DID and received message content, empty string if no message or error occurred - """ - try: - sender_did, message = await agent_connect_simple_node.receive_message() - if message: - print(f"Received message from {sender_did}: {message}") - return sender_did, message - return "", "" - except Exception as e: - print(f"Failed to receive message: {e}") - return "", "" diff --git a/agentstack/tools/agent_connect.json b/agentstack/tools/agent_connect/config.json similarity index 85% rename from agentstack/tools/agent_connect.json rename to agentstack/tools/agent_connect/config.json index 34922eba..3ea21201 100644 --- a/agentstack/tools/agent_connect.json +++ b/agentstack/tools/agent_connect/config.json @@ -1,6 +1,5 @@ { "name": "agent-connect", - "packages": ["agent-connect"], "env": "HOST_DOMAIN=...\nHOST_PORT=\"80\"\nHOST_WS_PATH=\"/ws\"\nDID_DOCUMENT_PATH=...\nSSL_CERT_PATH=...\nSSL_KEY_PATH=...", "tools": ["send_message", "receive_message"] } diff --git a/agentstack/tools/agent_connect/crew/__init__.py b/agentstack/tools/agent_connect/crew/__init__.py new file mode 100644 index 00000000..74935199 --- /dev/null +++ b/agentstack/tools/agent_connect/crew/__init__.py @@ -0,0 +1,41 @@ +from crewai_tools import tool +from .common import agent_connect_simple_node + + +@tool("Send Message to Agent by DID") +async def send_message(message: str, destination_did: str) -> bool: + """ + Send a message through agent-connect node. + + Args: + message: Message content to be sent + destination_did: DID of the recipient agent + Returns: + bool: True if message was sent successfully, False otherwise + """ + try: + await agent_connect_simple_node.send_message(message, destination_did) + print(f"Successfully sent message: {message}") + return True + except Exception as e: + print(f"Failed to send message: {e}") + return False + +@tool("Receive Message from Agent") +async def receive_message() -> tuple[str, str]: + """ + Receive message from agent-connect node. + + Returns: + tuple[str, str]: Sender DID and received message content, empty string if no message or error occurred + """ + try: + sender_did, message = await agent_connect_simple_node.receive_message() + if message: + print(f"Received message from {sender_did}: {message}") + return sender_did, message + return "", "" + except Exception as e: + print(f"Failed to receive message: {e}") + return "", "" + diff --git a/agentstack/tools/agent_connect/pyproject.toml b/agentstack/tools/agent_connect/pyproject.toml new file mode 100644 index 00000000..9444f556 --- /dev/null +++ b/agentstack/tools/agent_connect/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "agentstack.agent_connect" +version = "0.1" +description = "agent_connect implementation for AgentStack" +readme = "README.md" + +dependencies = [ + "agentstack >= 0.1.0", + "agent-connect >= 0.2.0", +] + +[project.optional-dependencies] +crew = [ + "crewai >= 0.83.0", +] + diff --git a/pyproject.toml b/pyproject.toml index 3c402e93..a8347c7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "pydantic>=2.10", "packaging>=24.2", "requests>=2.32", + "tomli >= 2; python_version < '3.11'", ] [tool.setuptools.package-data] diff --git a/tests/fixtures/tool_config_max.json b/tests/fixtures/tool_config_max.json new file mode 100644 index 00000000..1e5734d1 --- /dev/null +++ b/tests/fixtures/tool_config_max.json @@ -0,0 +1,14 @@ +{ + "name": "tool_name", + "category": "category", + "tools": ["tool1", "tool2"], + "url": "https://example.com", + "tools_bundled": true, + "cta": "Click me!", + "env": { + "ENV_VAR1": "value1", + "ENV_VAR2": "value2" + }, + "post_install": "install.sh", + "post_remove": "remove.sh" +} \ No newline at end of file diff --git a/tests/fixtures/tool_config_min.json b/tests/fixtures/tool_config_min.json new file mode 100644 index 00000000..a57f2233 --- /dev/null +++ b/tests/fixtures/tool_config_min.json @@ -0,0 +1,5 @@ +{ + "name": "tool_name", + "category": "category", + "tools": ["tool1", "tool2"] +} \ No newline at end of file From 4982c1411fce5901fac99ce97247832ff390bc56 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sun, 1 Dec 2024 16:29:47 -0800 Subject: [PATCH 02/34] Remove `packages` from ToolConfig tests. --- tests/test_tool_config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_tool_config.py b/tests/test_tool_config.py index 90931c7b..b1a3b789 100644 --- a/tests/test_tool_config.py +++ b/tests/test_tool_config.py @@ -17,7 +17,6 @@ def test_minimal_json(self): assert config.tools_bundled is False assert config.cta is None assert config.env is None - assert config.packages is None assert config.post_install is None assert config.post_remove is None @@ -30,7 +29,6 @@ def test_maximal_json(self): assert config.tools_bundled is True assert config.cta == "Click me!" assert config.env == {"ENV_VAR1": "value1", "ENV_VAR2": "value2"} - assert config.packages == ["package1", "package2"] assert config.post_install == "install.sh" assert config.post_remove == "remove.sh" From 3c7568e89ec2025e7d40b2ebc8ad1183a765af34 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sun, 1 Dec 2024 16:33:26 -0800 Subject: [PATCH 03/34] Tool package init file placeholder. --- agentstack/tools/agent_connect/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 agentstack/tools/agent_connect/__init__.py diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py new file mode 100644 index 00000000..228ed202 --- /dev/null +++ b/agentstack/tools/agent_connect/__init__.py @@ -0,0 +1,4 @@ +# TODO Some sort of framework-detection and conditional imports +# ie. from agentstack import get_framework +# if get_framework() == "crewai": +# from .crew import * \ No newline at end of file From bdbd9051b0515f86375cabe8f12860cbc8f294ab Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sun, 1 Dec 2024 16:42:33 -0800 Subject: [PATCH 04/34] Bug in ToolConfig.get_path --- agentstack/generation/tool_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 0d1b82f1..8b350f94 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -73,7 +73,7 @@ def get_import_statement(self) -> str: return f"from .{self.name}_tool import {', '.join(self.tools)}" def get_path(self) -> Path: - return get_package_path()/'tools'/tool_name + return get_package_path()/'tools'/self.name def get_impl_file_path(self, framework: str) -> Path: return get_package_path() / f'templates/{framework}/tools/{self.name}_tool.py' From 02d61f614c189447a839a09bf22ad204e0b0f1b3 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Sun, 1 Dec 2024 16:43:46 -0800 Subject: [PATCH 05/34] Merge cruft in tool_generation --- agentstack/generation/tool_generation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 8b350f94..798ef55c 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -1,6 +1,5 @@ import os, sys from typing import Optional, Any, List -import importlib.resources from pathlib import Path import json import sys @@ -106,8 +105,6 @@ def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[s sys.exit(1) tool_data = ToolConfig.from_tool_name(tool_name) - tool_file_path = importlib.resources.files(f'agentstack.templates.{framework}.tools') / f'{tool_name}_tool.py' - packaging.install_tool(tool_data, path) 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 From 610dc2abc5f8c976abd3f628e4467d58118827e6 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 3 Dec 2024 20:13:50 -0800 Subject: [PATCH 06/34] Reformat tool packages to new format. --- agentstack/generation/tool_generation.py | 42 +++++++------------ agentstack/packaging.py | 36 +++++++++++----- agentstack/tools/agent_connect/README.md | 23 ---------- agentstack/tools/agent_connect/__init__.py | 4 -- .../{common/__init__.py => common.py} | 0 agentstack/tools/agent_connect/config.json | 1 - .../{crew/__init__.py => crewai.py} | 0 agentstack/tools/agent_connect/pyproject.toml | 10 +---- agentstack/tools/browserbase/__init__.py | 0 .../config.json} | 1 - .../browserbase/crewai.py} | 0 agentstack/tools/browserbase/pyproject.toml | 10 +++++ agentstack/tools/code_interpreter/__init__.py | 0 .../config.json} | 0 .../code_interpreter/crewai.py} | 0 .../tools/code_interpreter/pyproject.toml | 8 ++++ agentstack/tools/composio/__init__.py | 0 .../{composio.json => composio/config.json} | 1 - .../composio/crewai.py} | 0 agentstack/tools/composio/pyproject.toml | 10 +++++ agentstack/tools/directory_search/__init__.py | 0 .../config.json} | 0 .../directory_search/crewai.py} | 0 .../tools/directory_search/pyproject.toml | 8 ++++ agentstack/tools/exa/__init__.py | 0 .../tools/exa_tool.py => tools/exa/common.py} | 5 +-- .../tools/{exa.json => exa/config.json} | 1 - agentstack/tools/exa/crewai.py | 8 ++++ agentstack/tools/exa/pyproject.toml | 10 +++++ agentstack/tools/file_read/__init__.py | 0 .../{file_read.json => file_read/config.json} | 0 .../file_read/crewai.py} | 0 agentstack/tools/file_read/pyproject.toml | 8 ++++ agentstack/tools/firecrawl/__init__.py | 0 .../firecrawl/common.py} | 1 - .../{firecrawl.json => firecrawl/config.json} | 1 - agentstack/tools/firecrawl/crewai.py | 22 ++++++++++ agentstack/tools/firecrawl/pyproject.toml | 10 +++++ agentstack/tools/ftp/__init__.py | 0 .../tools/ftp_tool.py => tools/ftp/common.py} | 4 -- .../tools/{ftp.json => ftp/config.json} | 1 - agentstack/tools/ftp/crewai.py | 5 +++ agentstack/tools/ftp/pyproject.toml | 8 ++++ agentstack/tools/mem0/__init__.py | 0 .../mem0_tool.py => tools/mem0/common.py} | 8 +--- .../tools/{mem0.json => mem0/config.json} | 1 - agentstack/tools/mem0/crewai.py | 5 +++ agentstack/tools/mem0/pyproject.toml | 10 +++++ agentstack/tools/neon/__init__.py | 0 .../neon_tool.py => tools/neon/common.py} | 6 --- .../tools/{neon.json => neon/config.json} | 1 - agentstack/tools/neon/crewai.py | 6 +++ agentstack/tools/neon/pyproject.toml | 11 +++++ agentstack/tools/open_interpreter/__init__.py | 0 .../open_interpreter/common.py} | 2 - .../config.json} | 1 - agentstack/tools/open_interpreter/crewai.py | 4 ++ .../tools/open_interpreter/pyproject.toml | 10 +++++ agentstack/tools/perplexity/__init__.py | 0 .../perplexity/common.py} | 6 +-- .../config.json} | 0 agentstack/tools/perplexity/crewai.py | 4 ++ agentstack/tools/perplexity/pyproject.toml | 10 +++++ agentstack/tools/stripe/__init__.py | 0 .../tools/{stripe.json => stripe/config.json} | 1 - .../stripe_tool.py => tools/stripe/crewai.py} | 5 +-- agentstack/tools/stripe/pyproject.toml | 11 +++++ agentstack/tools/vision/__init__.py | 0 .../tools/{vision.json => vision/config.json} | 0 .../vision_tool.py => tools/vision/crewai.py} | 2 +- agentstack/tools/vision/pyproject.toml | 8 ++++ agentstack/tools/~README.md | 33 ++++++++++++--- tests/test_tool_config.py | 2 +- 73 files changed, 263 insertions(+), 122 deletions(-) delete mode 100644 agentstack/tools/agent_connect/README.md rename agentstack/tools/agent_connect/{common/__init__.py => common.py} (100%) rename agentstack/tools/agent_connect/{crew/__init__.py => crewai.py} (100%) create mode 100644 agentstack/tools/browserbase/__init__.py rename agentstack/tools/{browserbase.json => browserbase/config.json} (86%) rename agentstack/{templates/crewai/tools/browserbase_tool.py => tools/browserbase/crewai.py} (100%) create mode 100644 agentstack/tools/browserbase/pyproject.toml create mode 100644 agentstack/tools/code_interpreter/__init__.py rename agentstack/tools/{code_interpreter.json => code_interpreter/config.json} (100%) rename agentstack/{templates/crewai/tools/code_interpreter_tool.py => tools/code_interpreter/crewai.py} (100%) create mode 100644 agentstack/tools/code_interpreter/pyproject.toml create mode 100644 agentstack/tools/composio/__init__.py rename agentstack/tools/{composio.json => composio/config.json} (89%) rename agentstack/{templates/crewai/tools/composio_tool.py => tools/composio/crewai.py} (100%) create mode 100644 agentstack/tools/composio/pyproject.toml create mode 100644 agentstack/tools/directory_search/__init__.py rename agentstack/tools/{directory_search.json => directory_search/config.json} (100%) rename agentstack/{templates/crewai/tools/directory_search_tool.py => tools/directory_search/crewai.py} (100%) create mode 100644 agentstack/tools/directory_search/pyproject.toml create mode 100644 agentstack/tools/exa/__init__.py rename agentstack/{templates/crewai/tools/exa_tool.py => tools/exa/common.py} (89%) rename agentstack/tools/{exa.json => exa/config.json} (89%) create mode 100644 agentstack/tools/exa/crewai.py create mode 100644 agentstack/tools/exa/pyproject.toml create mode 100644 agentstack/tools/file_read/__init__.py rename agentstack/tools/{file_read.json => file_read/config.json} (100%) rename agentstack/{templates/crewai/tools/file_read_tool.py => tools/file_read/crewai.py} (100%) create mode 100644 agentstack/tools/file_read/pyproject.toml create mode 100644 agentstack/tools/firecrawl/__init__.py rename agentstack/{templates/crewai/tools/firecrawl_tool.py => tools/firecrawl/common.py} (98%) rename agentstack/tools/{firecrawl.json => firecrawl/config.json} (89%) create mode 100644 agentstack/tools/firecrawl/crewai.py create mode 100644 agentstack/tools/firecrawl/pyproject.toml create mode 100644 agentstack/tools/ftp/__init__.py rename agentstack/{templates/crewai/tools/ftp_tool.py => tools/ftp/common.py} (93%) rename agentstack/tools/{ftp.json => ftp/config.json} (92%) create mode 100644 agentstack/tools/ftp/crewai.py create mode 100644 agentstack/tools/ftp/pyproject.toml create mode 100644 agentstack/tools/mem0/__init__.py rename agentstack/{templates/crewai/tools/mem0_tool.py => tools/mem0/common.py} (89%) rename agentstack/tools/{mem0.json => mem0/config.json} (90%) create mode 100644 agentstack/tools/mem0/crewai.py create mode 100644 agentstack/tools/mem0/pyproject.toml create mode 100644 agentstack/tools/neon/__init__.py rename agentstack/{templates/crewai/tools/neon_tool.py => tools/neon/common.py} (93%) rename agentstack/tools/{neon.json => neon/config.json} (85%) create mode 100644 agentstack/tools/neon/crewai.py create mode 100644 agentstack/tools/neon/pyproject.toml create mode 100644 agentstack/tools/open_interpreter/__init__.py rename agentstack/{templates/crewai/tools/open_interpreter_tool.py => tools/open_interpreter/common.py} (90%) rename agentstack/tools/{open_interpreter.json => open_interpreter/config.json} (81%) create mode 100644 agentstack/tools/open_interpreter/crewai.py create mode 100644 agentstack/tools/open_interpreter/pyproject.toml create mode 100644 agentstack/tools/perplexity/__init__.py rename agentstack/{templates/crewai/tools/perplexity_tool.py => tools/perplexity/common.py} (94%) rename agentstack/tools/{perplexity.json => perplexity/config.json} (100%) create mode 100644 agentstack/tools/perplexity/crewai.py create mode 100644 agentstack/tools/perplexity/pyproject.toml create mode 100644 agentstack/tools/stripe/__init__.py rename agentstack/tools/{stripe.json => stripe/config.json} (86%) rename agentstack/{templates/crewai/tools/stripe_tool.py => tools/stripe/crewai.py} (91%) create mode 100644 agentstack/tools/stripe/pyproject.toml create mode 100644 agentstack/tools/vision/__init__.py rename agentstack/tools/{vision.json => vision/config.json} (100%) rename agentstack/{templates/crewai/tools/vision_tool.py => tools/vision/crewai.py} (57%) create mode 100644 agentstack/tools/vision/pyproject.toml diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 012587af..fa7c99e2 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -23,20 +23,7 @@ TOOL_INIT_FILENAME = "src/tools/__init__.py" -FRAMEWORK_FILENAMES: dict[str, str] = { - 'crewai': 'src/crew.py', -} - -def get_framework_filename(framework: str, path: str = ''): - if path: - path = path.endswith('/') and path or path + '/' - else: - path = './' - try: - return f"{path}{FRAMEWORK_FILENAMES[framework]}" - except KeyError: - print(term_color(f'Unknown framework: {framework}', 'red')) - sys.exit(1) +TOOL_CONFIG_FILENAME = "config.json" class ToolConfig(BaseModel): name: str @@ -51,37 +38,36 @@ class ToolConfig(BaseModel): @classmethod def from_tool_name(cls, name: str) -> 'ToolConfig': - path = get_package_path() / f'tools/{name}.json' + path = get_package_path()/'tools'/name/TOOL_CONFIG_FILENAME if not os.path.exists(path): print(term_color(f'No known agentstack tool: {name}', 'red')) sys.exit(1) return cls.from_json(path) @classmethod - def from_json(cls, path: Path) -> 'ToolConfig': - data = open_json_file(path) + def from_json(cls, filename: Path) -> 'ToolConfig': + data = open_json_file(filename) try: return cls(**data) except ValidationError as e: - print(term_color(f"Error validating tool config JSON: \n{path}", 'red')) + print(term_color(f"Error validating tool config JSON: \n{filename}", 'red')) 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_import_statement(self, framework: str) -> str: + return f"from .{self.name}.{framework} import {', '.join(self.tools)}" def get_path(self) -> Path: return get_package_path()/'tools'/self.name - 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' + tools_dir = get_package_path()/'tools' for file in tools_dir.iterdir(): - if file.is_file() and file.suffix == '.json': + # TODO packaging.validate_tool + if file.is_dir() and (file/TOOL_CONFIG_FILENAME).exists(): paths.append(file) return paths @@ -89,7 +75,7 @@ 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()] + return [ToolConfig.from_json(path/TOOL_CONFIG_FILENAME) for path in get_all_tool_paths()] def add_tool(tool_name: str, path: Optional[str] = None, agents: Optional[List[str]] = []): if path: @@ -156,16 +142,18 @@ def remove_tool(tool_name: str, path: Optional[str] = None): def add_tool_to_tools_init(tool_data: ToolConfig, path: str = ''): + framework = get_framework(path) 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(framework), ] 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.""" + framework = get_framework(path) file_path = f'{path}{TOOL_INIT_FILENAME}' - import_statement = tool_data.get_import_statement() + import_statement = tool_data.get_import_statement(framework) with fileinput.input(files=file_path, inplace=True) as f: for line in f: if line.strip() != import_statement: diff --git a/agentstack/packaging.py b/agentstack/packaging.py index a4ede7ff..25cba2df 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -8,6 +8,7 @@ import os, sys import shutil from typing import TYPE_CHECKING, Optional +from pathlib import Path from packaging.metadata import Metadata from agentstack.utils import term_color, get_package_path, get_framework if TYPE_CHECKING: # TODO move ToolConfig to a separate file @@ -27,7 +28,7 @@ def install(self, *args): def upgrade(self, *args): os.system(f"{self.cmd} {self.update} {' '.join(args)}") - def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = None): + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[Path] = None): """ Copy a tool's source files from the agentstack install directory to the app tools directory and install relevant dependencies. @@ -39,7 +40,7 @@ def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = """ raise NotImplementedError - def remove_tool(self, tool: 'ToolConfig', path: Optional[str] = None): + def remove_tool(self, tool: 'ToolConfig', path: Optional[Path] = None): """ Remove a tool's source files from the app tools directory. @@ -52,18 +53,20 @@ class UV(PackageManager): def __init__(self): super().__init__('uv', 'add', 'pip install --upgrade') - def install_tool(self, tool: 'ToolConfig', path: Optional[str] = None): + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[Path] = None): # uv pip install --directory --target --link-mode copy --editable .[] - os.system(f"{self.cmd} pip install --link-mode copy --directory {tool.get_path()} --target {path}src/tools/ --editable .[{framework}]") + if path is None: path = Path('') + os.system(f"{self.cmd} pip install --link-mode copy --directory {tool.get_path()} --target {path}src/tools/ --editable .") class Poetry(PackageManager): def __init__(self): super().__init__('poetry', 'add', 'update') - def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = None): + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[Path] = None): # poetry doesn't have a way to install into a target directory afaik so we # first install the dependencies and then manually copy the tool files # poetry install --no-root --directory --with + if path is None: path = Path('') arg = f' --directory {package_path}' if optional_dependencies_tag: arg += f" --with {','.join(optional_dependencies_tags)}" @@ -74,8 +77,9 @@ class PIP(PackageManager): def __init__(self): super().__init__('pip', 'install', 'install --upgrade') - def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[str] = None): + def install_tool(self, tool: 'ToolConfig', framework: str, path: Optional[Path] = None): # pip install --target --editable .[] + if path is None: path = Path('') os.system(f"{self.cmd} {self.install} --target {path}src/tools/ --editable {tool.get_path()}[{framework}]") UV = UV() @@ -102,12 +106,14 @@ def detect_package_manager() -> PackageManager: print(', '.join(manager.cmd for manager in PACKAGE_MANAGERS)) sys.exit(1) -def install(package: str, path: Optional[str] = None): +def install(package: str, path: Optional[Path] = None): if path: os.chdir(path) detect_package_manager().install(package) -def upgrade(package: str): +def upgrade(package: str, path: Optional[Path] = None): + if path: + os.chdir(path) detect_package_manager().upgrade(package) def get_tool_metadata(tool: 'ToolConfig') -> Metadata: @@ -117,7 +123,17 @@ def get_tool_metadata(tool: 'ToolConfig') -> Metadata: """ return Metadata.from_pyproject(get_tool_path()/'pyproject.toml') -def install_tool(tool: 'ToolConfig', path: Optional[str] = None): +def validate_tool(tool: 'ToolConfig') -> bool: + """ + Check if the tool is installed correctly + """ + metadata = get_tool_metadata(tool) + # TODO validate metadata + # TODO ensure framework implementations are present + # TODO tests? + raise NotImplementedError + +def install_tool(tool: 'ToolConfig', path: Optional[Path] = None): """ Copy a tool's applicable source files and install relevant dependencies """ @@ -125,7 +141,7 @@ def install_tool(tool: 'ToolConfig', path: Optional[str] = None): manager = detect_package_manager() manager.install_tool(tool, framework, path) -def remove_tool(tool: 'ToolConfig', path: Optional[str] = None): +def remove_tool(tool: 'ToolConfig', path: Optional[Path] = None): """ Remove a tool's source files. Removing dependencies is messy, so we just leave them installed. diff --git a/agentstack/tools/agent_connect/README.md b/agentstack/tools/agent_connect/README.md deleted file mode 100644 index 815fa2d3..00000000 --- a/agentstack/tools/agent_connect/README.md +++ /dev/null @@ -1,23 +0,0 @@ - - -Tool Implementation -=================== - -`config.json` -------------- -This contains the configuration for the tool for use by agentstack. - -`pyproject.toml` ----------------- -This contains the build configuration for the tool. - -`crew` ------- -Python package which contains the tool implementation specific to CrewAI. -Use relative imports to include shared code from the root of the tool's package. -Deploy/install command should handle both single files and directories. - -Additional frameworks will get their own directories. - - - diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py index 228ed202..e69de29b 100644 --- a/agentstack/tools/agent_connect/__init__.py +++ b/agentstack/tools/agent_connect/__init__.py @@ -1,4 +0,0 @@ -# TODO Some sort of framework-detection and conditional imports -# ie. from agentstack import get_framework -# if get_framework() == "crewai": -# from .crew import * \ No newline at end of file diff --git a/agentstack/tools/agent_connect/common/__init__.py b/agentstack/tools/agent_connect/common.py similarity index 100% rename from agentstack/tools/agent_connect/common/__init__.py rename to agentstack/tools/agent_connect/common.py diff --git a/agentstack/tools/agent_connect/config.json b/agentstack/tools/agent_connect/config.json index 3dd6a034..eba04684 100644 --- a/agentstack/tools/agent_connect/config.json +++ b/agentstack/tools/agent_connect/config.json @@ -2,7 +2,6 @@ "name": "agent_connect", "url": "https://github.com/chgaowei/AgentConnect", "category": "network-protocols", - "packages": ["agent-connect"], "env": { "HOST_DOMAIN": "...", "HOST_PORT": 80, diff --git a/agentstack/tools/agent_connect/crew/__init__.py b/agentstack/tools/agent_connect/crewai.py similarity index 100% rename from agentstack/tools/agent_connect/crew/__init__.py rename to agentstack/tools/agent_connect/crewai.py diff --git a/agentstack/tools/agent_connect/pyproject.toml b/agentstack/tools/agent_connect/pyproject.toml index 9444f556..532b90c8 100644 --- a/agentstack/tools/agent_connect/pyproject.toml +++ b/agentstack/tools/agent_connect/pyproject.toml @@ -1,16 +1,10 @@ [project] name = "agentstack.agent_connect" version = "0.1" -description = "agent_connect implementation for AgentStack" -readme = "README.md" dependencies = [ - "agentstack >= 0.1.0", - "agent-connect >= 0.2.0", + "agent-connect", ] [project.optional-dependencies] -crew = [ - "crewai >= 0.83.0", -] - +crewai = [] \ No newline at end of file diff --git a/agentstack/tools/browserbase/__init__.py b/agentstack/tools/browserbase/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/browserbase.json b/agentstack/tools/browserbase/config.json similarity index 86% rename from agentstack/tools/browserbase.json rename to agentstack/tools/browserbase/config.json index d005c278..1d3b83c2 100644 --- a/agentstack/tools/browserbase.json +++ b/agentstack/tools/browserbase/config.json @@ -2,7 +2,6 @@ "name": "browserbase", "url": "https://github.com/browserbase/python-sdk", "category": "browsing", - "packages": ["browserbase", "playwright"], "env": { "BROWSERBASE_API_KEY": "...", "BROWSERBASE_PROJECT_ID": "..." diff --git a/agentstack/templates/crewai/tools/browserbase_tool.py b/agentstack/tools/browserbase/crewai.py similarity index 100% rename from agentstack/templates/crewai/tools/browserbase_tool.py rename to agentstack/tools/browserbase/crewai.py diff --git a/agentstack/tools/browserbase/pyproject.toml b/agentstack/tools/browserbase/pyproject.toml new file mode 100644 index 00000000..76f8a053 --- /dev/null +++ b/agentstack/tools/browserbase/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.browserbase" +version = "0.1" + +dependencies = [ + "browserbase", +] + +[project.optional-dependencies] +crewai = [] \ No newline at end of file diff --git a/agentstack/tools/code_interpreter/__init__.py b/agentstack/tools/code_interpreter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/code_interpreter.json b/agentstack/tools/code_interpreter/config.json similarity index 100% rename from agentstack/tools/code_interpreter.json rename to agentstack/tools/code_interpreter/config.json diff --git a/agentstack/templates/crewai/tools/code_interpreter_tool.py b/agentstack/tools/code_interpreter/crewai.py similarity index 100% rename from agentstack/templates/crewai/tools/code_interpreter_tool.py rename to agentstack/tools/code_interpreter/crewai.py diff --git a/agentstack/tools/code_interpreter/pyproject.toml b/agentstack/tools/code_interpreter/pyproject.toml new file mode 100644 index 00000000..f630d2b1 --- /dev/null +++ b/agentstack/tools/code_interpreter/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "agentstack.code_interpreter" +version = "0.1" + +dependencies = [] + +[project.optional-dependencies] +crewai = [] \ No newline at end of file diff --git a/agentstack/tools/composio/__init__.py b/agentstack/tools/composio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/composio.json b/agentstack/tools/composio/config.json similarity index 89% rename from agentstack/tools/composio.json rename to agentstack/tools/composio/config.json index c2b20d0d..5a5dfb33 100644 --- a/agentstack/tools/composio.json +++ b/agentstack/tools/composio/config.json @@ -2,7 +2,6 @@ "name": "composio", "url": "https://composio.dev/", "category": "unified-apis", - "packages": ["composio-crewai"], "env": { "COMPOSIO_API_KEY": "..." }, diff --git a/agentstack/templates/crewai/tools/composio_tool.py b/agentstack/tools/composio/crewai.py similarity index 100% rename from agentstack/templates/crewai/tools/composio_tool.py rename to agentstack/tools/composio/crewai.py diff --git a/agentstack/tools/composio/pyproject.toml b/agentstack/tools/composio/pyproject.toml new file mode 100644 index 00000000..4afbc383 --- /dev/null +++ b/agentstack/tools/composio/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.composio" +version = "0.1" + +dependencies = [] + +[project.optional-dependencies] +crewai = [ + "composio-crewai" +] diff --git a/agentstack/tools/directory_search/__init__.py b/agentstack/tools/directory_search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/directory_search.json b/agentstack/tools/directory_search/config.json similarity index 100% rename from agentstack/tools/directory_search.json rename to agentstack/tools/directory_search/config.json diff --git a/agentstack/templates/crewai/tools/directory_search_tool.py b/agentstack/tools/directory_search/crewai.py similarity index 100% rename from agentstack/templates/crewai/tools/directory_search_tool.py rename to agentstack/tools/directory_search/crewai.py diff --git a/agentstack/tools/directory_search/pyproject.toml b/agentstack/tools/directory_search/pyproject.toml new file mode 100644 index 00000000..b575386f --- /dev/null +++ b/agentstack/tools/directory_search/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "agentstack.directory_search" +version = "0.1" + +dependencies = [] + +[project.optional-dependencies] +crewai = [] \ No newline at end of file diff --git a/agentstack/tools/exa/__init__.py b/agentstack/tools/exa/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/exa_tool.py b/agentstack/tools/exa/common.py similarity index 89% rename from agentstack/templates/crewai/tools/exa_tool.py rename to agentstack/tools/exa/common.py index 9eb2e194..867055bb 100644 --- a/agentstack/templates/crewai/tools/exa_tool.py +++ b/agentstack/tools/exa/common.py @@ -1,12 +1,9 @@ -from crewai_tools import tool from exa_py import Exa -from dotenv import load_dotenv import os -load_dotenv() # Check out our docs for more info! https://docs.exa.ai/ -@tool("Exa search and get contents") + def search_and_contents(question: str) -> str: """ Tool using Exa's Python SDK to run semantic search and return result highlights. diff --git a/agentstack/tools/exa.json b/agentstack/tools/exa/config.json similarity index 89% rename from agentstack/tools/exa.json rename to agentstack/tools/exa/config.json index 2dada5e3..5ab36b4d 100644 --- a/agentstack/tools/exa.json +++ b/agentstack/tools/exa/config.json @@ -2,7 +2,6 @@ "name": "exa", "url": "https://exa.ai", "category": "web-retrieval", - "packages": ["exa_py"], "env": { "EXA_API_KEY": "..." }, diff --git a/agentstack/tools/exa/crewai.py b/agentstack/tools/exa/crewai.py new file mode 100644 index 00000000..d71e9462 --- /dev/null +++ b/agentstack/tools/exa/crewai.py @@ -0,0 +1,8 @@ +from crewai_tools import tool +from .common import search_and_contents as _search_and_contents + + +@tool("Exa search and get contents") +def search_and_contents(question: str) -> str: + return _search_and_contents(question) + diff --git a/agentstack/tools/exa/pyproject.toml b/agentstack/tools/exa/pyproject.toml new file mode 100644 index 00000000..03d7eb66 --- /dev/null +++ b/agentstack/tools/exa/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.exa" +version = "0.1" + +dependencies = [ + "exa_py" +] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/file_read/__init__.py b/agentstack/tools/file_read/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/file_read.json b/agentstack/tools/file_read/config.json similarity index 100% rename from agentstack/tools/file_read.json rename to agentstack/tools/file_read/config.json diff --git a/agentstack/templates/crewai/tools/file_read_tool.py b/agentstack/tools/file_read/crewai.py similarity index 100% rename from agentstack/templates/crewai/tools/file_read_tool.py rename to agentstack/tools/file_read/crewai.py diff --git a/agentstack/tools/file_read/pyproject.toml b/agentstack/tools/file_read/pyproject.toml new file mode 100644 index 00000000..a4f20180 --- /dev/null +++ b/agentstack/tools/file_read/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "agentstack.file_read" +version = "0.1" + +dependencies = [] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/firecrawl/__init__.py b/agentstack/tools/firecrawl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/firecrawl_tool.py b/agentstack/tools/firecrawl/common.py similarity index 98% rename from agentstack/templates/crewai/tools/firecrawl_tool.py rename to agentstack/tools/firecrawl/common.py index 65c66c24..d1ed9268 100644 --- a/agentstack/templates/crewai/tools/firecrawl_tool.py +++ b/agentstack/tools/firecrawl/common.py @@ -1,4 +1,3 @@ -from crewai_tools import tool from firecrawl import FirecrawlApp import os diff --git a/agentstack/tools/firecrawl.json b/agentstack/tools/firecrawl/config.json similarity index 89% rename from agentstack/tools/firecrawl.json rename to agentstack/tools/firecrawl/config.json index 7937fde4..21f8aa20 100644 --- a/agentstack/tools/firecrawl.json +++ b/agentstack/tools/firecrawl/config.json @@ -2,7 +2,6 @@ "name": "firecrawl", "url": "https://www.firecrawl.dev/", "category": "browsing", - "packages": ["firecrawl-py"], "env": { "FIRECRAWL_API_KEY": "..." }, diff --git a/agentstack/tools/firecrawl/crewai.py b/agentstack/tools/firecrawl/crewai.py new file mode 100644 index 00000000..fb57dc01 --- /dev/null +++ b/agentstack/tools/firecrawl/crewai.py @@ -0,0 +1,22 @@ +from crewai_tools import tool +from .common import ( + web_scrape as _web_scrape, + web_crawl as _web_crawl, + retrieve_web_crawl as _retrieve_web_crawl, +) + + +@tool("Web Scrape") +def web_scrape(url: str): + return _web_scrape(url) + + +@tool("Web Crawl") +def web_crawl(url: str): + return _web_crawl(url) + + +@tool("Retrieve Web Crawl") +def retrieve_web_crawl(crawl_id: str): + return _retrieve_web_crawl(crawl_id) + diff --git a/agentstack/tools/firecrawl/pyproject.toml b/agentstack/tools/firecrawl/pyproject.toml new file mode 100644 index 00000000..f4051627 --- /dev/null +++ b/agentstack/tools/firecrawl/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.firecrawl" +version = "0.1" + +dependencies = [ + "firecrawl-py", +] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/ftp/__init__.py b/agentstack/tools/ftp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/ftp_tool.py b/agentstack/tools/ftp/common.py similarity index 93% rename from agentstack/templates/crewai/tools/ftp_tool.py rename to agentstack/tools/ftp/common.py index cb80f0ff..d09348c4 100644 --- a/agentstack/templates/crewai/tools/ftp_tool.py +++ b/agentstack/tools/ftp/common.py @@ -1,9 +1,6 @@ from ftplib import FTP -from crewai_tools import tool -from dotenv import load_dotenv import os -load_dotenv() # FTP server details ftp_host = os.getenv('FTP_HOST') @@ -12,7 +9,6 @@ ftp_path = '/' -@tool def upload_files(file_paths: list[str]): """Upload a list of files to the FTP server.""" diff --git a/agentstack/tools/ftp.json b/agentstack/tools/ftp/config.json similarity index 92% rename from agentstack/tools/ftp.json rename to agentstack/tools/ftp/config.json index ca11fcc8..b6dfc870 100644 --- a/agentstack/tools/ftp.json +++ b/agentstack/tools/ftp/config.json @@ -1,7 +1,6 @@ { "name": "ftp", "category": "computer-control", - "packages": [], "env": { "FTP_HOST": "...", "FTP_USER": "...", diff --git a/agentstack/tools/ftp/crewai.py b/agentstack/tools/ftp/crewai.py new file mode 100644 index 00000000..0e4abc62 --- /dev/null +++ b/agentstack/tools/ftp/crewai.py @@ -0,0 +1,5 @@ +from crewai_tools import tool +from .commonn import upload_files + + +upload_files = tool("Upload Files to FTP Server")(upload_files) diff --git a/agentstack/tools/ftp/pyproject.toml b/agentstack/tools/ftp/pyproject.toml new file mode 100644 index 00000000..5f9df481 --- /dev/null +++ b/agentstack/tools/ftp/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "agentstack.ftp" +version = "0.1" + +dependencies = [] + +[project.optional-dependencies] +crewai = [] \ No newline at end of file diff --git a/agentstack/tools/mem0/__init__.py b/agentstack/tools/mem0/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/mem0_tool.py b/agentstack/tools/mem0/common.py similarity index 89% rename from agentstack/templates/crewai/tools/mem0_tool.py rename to agentstack/tools/mem0/common.py index 44a7d93a..3ffbe332 100644 --- a/agentstack/templates/crewai/tools/mem0_tool.py +++ b/agentstack/tools/mem0/common.py @@ -1,12 +1,7 @@ import json - -from crewai_tools import tool from mem0 import MemoryClient -from dotenv import load_dotenv import os -load_dotenv() - # These functions can be extended by changing the user_id parameter # Memories are sorted by user_id @@ -17,7 +12,7 @@ # "Potato is a vegetable" is not a memory # "My favorite food is potatoes" IS a memory -@tool("Write to Memory") + def write_to_memory(user_message: str) -> str: """ Writes data to the memory store for a user. The tool will decide what @@ -30,7 +25,6 @@ def write_to_memory(user_message: str) -> str: return json.dumps(result) -@tool("Read from Memory") def read_from_memory(query: str) -> str: """ Reads memories related to user based on a query. diff --git a/agentstack/tools/mem0.json b/agentstack/tools/mem0/config.json similarity index 90% rename from agentstack/tools/mem0.json rename to agentstack/tools/mem0/config.json index dfd224ac..fe71cc64 100644 --- a/agentstack/tools/mem0.json +++ b/agentstack/tools/mem0/config.json @@ -2,7 +2,6 @@ "name": "mem0", "url": "https://github.com/mem0ai/mem0", "category": "storage", - "packages": ["mem0ai"], "env": { "MEM0_API_KEY": "..." }, diff --git a/agentstack/tools/mem0/crewai.py b/agentstack/tools/mem0/crewai.py new file mode 100644 index 00000000..e10253ec --- /dev/null +++ b/agentstack/tools/mem0/crewai.py @@ -0,0 +1,5 @@ +from crewai_tools import tool +from .common import write_to_memory, read_from_memory + +write_to_memory = tool("Write to Memory")(write_to_memory) +read_from_memory = tool("Read from Memory")(read_from_memory) diff --git a/agentstack/tools/mem0/pyproject.toml b/agentstack/tools/mem0/pyproject.toml new file mode 100644 index 00000000..febb45d2 --- /dev/null +++ b/agentstack/tools/mem0/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.mem0" +version = "0.1" + +dependencies = [ + "mem0ai", +] + +[project.optional-dependencies] +crewai = [] \ No newline at end of file diff --git a/agentstack/tools/neon/__init__.py b/agentstack/tools/neon/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/neon_tool.py b/agentstack/tools/neon/common.py similarity index 93% rename from agentstack/templates/crewai/tools/neon_tool.py rename to agentstack/tools/neon/common.py index a00e907d..e2430a1e 100644 --- a/agentstack/templates/crewai/tools/neon_tool.py +++ b/agentstack/tools/neon/common.py @@ -1,17 +1,13 @@ import os -from crewai_tools import tool -from dotenv import load_dotenv from neon_api import NeonAPI import psycopg2 from psycopg2.extras import RealDictCursor -load_dotenv() NEON_API_KEY = os.getenv("NEON_API_KEY") neon_client = NeonAPI(api_key=NEON_API_KEY) -@tool("Create Neon Project and Database") def create_database(project_name: str) -> str: """ Creates a new Neon project. (this takes less than 500ms) @@ -30,7 +26,6 @@ def create_database(project_name: str) -> str: return f"Failed to create project: {str(e)}" -@tool("Execute SQL DDL") def execute_sql_ddl(connection_uri: str, command: str) -> str: """ Inserts data into a specified Neon database. @@ -53,7 +48,6 @@ def execute_sql_ddl(connection_uri: str, command: str) -> str: return f"Command succeeded" -@tool("Execute SQL DML") def run_sql_query(connection_uri: str, query: str) -> str: """ Inserts data into a specified Neon database. diff --git a/agentstack/tools/neon.json b/agentstack/tools/neon/config.json similarity index 85% rename from agentstack/tools/neon.json rename to agentstack/tools/neon/config.json index 8fd13f6d..193fc230 100644 --- a/agentstack/tools/neon.json +++ b/agentstack/tools/neon/config.json @@ -2,7 +2,6 @@ "name": "neon", "category": "database", "url": "https://github.com/neondatabase/neon", - "packages": ["neon-api", "psycopg2-binary"], "env": { "NEON_API_KEY": "..." }, diff --git a/agentstack/tools/neon/crewai.py b/agentstack/tools/neon/crewai.py new file mode 100644 index 00000000..d70fa719 --- /dev/null +++ b/agentstack/tools/neon/crewai.py @@ -0,0 +1,6 @@ +from crewai_tools import tool +from .common import create_database, execute_sql_ddl, run_sql_query + +create_database = tool("Create Neon Project and Database")(create_database) +execute_sql_ddl = tool("Execute SQL DDL")(execute_sql_ddl) +run_sql_query = tool("Execute SQL DML")(run_sql_query) diff --git a/agentstack/tools/neon/pyproject.toml b/agentstack/tools/neon/pyproject.toml new file mode 100644 index 00000000..eba069d3 --- /dev/null +++ b/agentstack/tools/neon/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "agentstack.neon" +version = "0.1" + +dependencies = [ + "neon-api", + "psycopg2-binary", +] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/open_interpreter/__init__.py b/agentstack/tools/open_interpreter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/open_interpreter_tool.py b/agentstack/tools/open_interpreter/common.py similarity index 90% rename from agentstack/templates/crewai/tools/open_interpreter_tool.py rename to agentstack/tools/open_interpreter/common.py index 55425b5f..8e8b9283 100644 --- a/agentstack/templates/crewai/tools/open_interpreter_tool.py +++ b/agentstack/tools/open_interpreter/common.py @@ -1,5 +1,4 @@ from interpreter import interpreter -from crewai_tools import tool # 1. Configuration and Tools @@ -7,7 +6,6 @@ interpreter.llm.model = "gpt-4o" -@tool def execute_code(code: str): """A tool to execute code using Open Interpreter. Returns the output of the code.""" result = interpreter.chat(f"execute this code with no changes: {code}") diff --git a/agentstack/tools/open_interpreter.json b/agentstack/tools/open_interpreter/config.json similarity index 81% rename from agentstack/tools/open_interpreter.json rename to agentstack/tools/open_interpreter/config.json index 238d035d..25c6abe0 100644 --- a/agentstack/tools/open_interpreter.json +++ b/agentstack/tools/open_interpreter/config.json @@ -2,6 +2,5 @@ "name": "open_interpreter", "url": "https://github.com/OpenInterpreter/open-interpreter", "category": "code-execution", - "packages": ["open-interpreter"], "tools": ["execute_code"] } \ No newline at end of file diff --git a/agentstack/tools/open_interpreter/crewai.py b/agentstack/tools/open_interpreter/crewai.py new file mode 100644 index 00000000..8615b32d --- /dev/null +++ b/agentstack/tools/open_interpreter/crewai.py @@ -0,0 +1,4 @@ +from crewai_tools import tool +from .common import execute_code + +execute_code = tool("Execute Code")(execute_code) diff --git a/agentstack/tools/open_interpreter/pyproject.toml b/agentstack/tools/open_interpreter/pyproject.toml new file mode 100644 index 00000000..79631f87 --- /dev/null +++ b/agentstack/tools/open_interpreter/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.open_interpreter" +version = "0.1" + +dependencies = [ + "open-interpreter", +] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/perplexity/__init__.py b/agentstack/tools/perplexity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/templates/crewai/tools/perplexity_tool.py b/agentstack/tools/perplexity/common.py similarity index 94% rename from agentstack/templates/crewai/tools/perplexity_tool.py rename to agentstack/tools/perplexity/common.py index a5a361b5..f13d5d51 100644 --- a/agentstack/templates/crewai/tools/perplexity_tool.py +++ b/agentstack/tools/perplexity/common.py @@ -1,15 +1,11 @@ import os - import requests -from crewai_tools import tool -from dotenv import load_dotenv -load_dotenv() url = "https://api.perplexity.ai/chat/completions" api_key = os.getenv("PERPLEXITY_API_KEY") -@tool + def query_perplexity(query: str): """ Use Perplexity to concisely search the internet and answer a query with up-to-date information. diff --git a/agentstack/tools/perplexity.json b/agentstack/tools/perplexity/config.json similarity index 100% rename from agentstack/tools/perplexity.json rename to agentstack/tools/perplexity/config.json diff --git a/agentstack/tools/perplexity/crewai.py b/agentstack/tools/perplexity/crewai.py new file mode 100644 index 00000000..598c92c0 --- /dev/null +++ b/agentstack/tools/perplexity/crewai.py @@ -0,0 +1,4 @@ +from crewai_tools import tool +from .common import query_perplexity + +query_perplexity = tool("Query Perplexity")(query_perplexity) diff --git a/agentstack/tools/perplexity/pyproject.toml b/agentstack/tools/perplexity/pyproject.toml new file mode 100644 index 00000000..1b34cd56 --- /dev/null +++ b/agentstack/tools/perplexity/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "agentstack.perplexity" +version = "0.1" + +dependencies = [ + "open-interpreter", +] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/stripe/__init__.py b/agentstack/tools/stripe/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/stripe.json b/agentstack/tools/stripe/config.json similarity index 86% rename from agentstack/tools/stripe.json rename to agentstack/tools/stripe/config.json index 212c6b23..9daa7d09 100644 --- a/agentstack/tools/stripe.json +++ b/agentstack/tools/stripe/config.json @@ -2,7 +2,6 @@ "name": "stripe", "url": "https://github.com/stripe/agent-toolkit", "category": "application-specific", - "packages": ["stripe-agent-toolkit", "stripe"], "env": { "STRIPE_SECRET_KEY": "sk-..." }, diff --git a/agentstack/templates/crewai/tools/stripe_tool.py b/agentstack/tools/stripe/crewai.py similarity index 91% rename from agentstack/templates/crewai/tools/stripe_tool.py rename to agentstack/tools/stripe/crewai.py index 864ea104..0a668e13 100644 --- a/agentstack/templates/crewai/tools/stripe_tool.py +++ b/agentstack/tools/stripe/crewai.py @@ -1,7 +1,5 @@ -import os from stripe_agent_toolkit.crewai.toolkit import StripeAgentToolkit -from dotenv import load_dotenv -load_dotenv() + stripe_tools = StripeAgentToolkit( secret_key=os.getenv("STRIPE_SECRET_KEY"), @@ -22,3 +20,4 @@ }, } }).get_tools() + diff --git a/agentstack/tools/stripe/pyproject.toml b/agentstack/tools/stripe/pyproject.toml new file mode 100644 index 00000000..bc427455 --- /dev/null +++ b/agentstack/tools/stripe/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "agentstack.stripe" +version = "0.1" + +dependencies = [ + "stripe-agent-toolkit", + "stripe", +] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/vision/__init__.py b/agentstack/tools/vision/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentstack/tools/vision.json b/agentstack/tools/vision/config.json similarity index 100% rename from agentstack/tools/vision.json rename to agentstack/tools/vision/config.json diff --git a/agentstack/templates/crewai/tools/vision_tool.py b/agentstack/tools/vision/crewai.py similarity index 57% rename from agentstack/templates/crewai/tools/vision_tool.py rename to agentstack/tools/vision/crewai.py index 927ca440..b2434b96 100644 --- a/agentstack/templates/crewai/tools/vision_tool.py +++ b/agentstack/tools/vision/crewai.py @@ -1,3 +1,3 @@ from crewai_tools import VisionTool -vision_tool = VisionTool() \ No newline at end of file +vision_tool = VisionTool() diff --git a/agentstack/tools/vision/pyproject.toml b/agentstack/tools/vision/pyproject.toml new file mode 100644 index 00000000..cf61ae98 --- /dev/null +++ b/agentstack/tools/vision/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "agentstack.vision" +version = "0.1" + +dependencies = [] + +[project.optional-dependencies] +crewai = [] diff --git a/agentstack/tools/~README.md b/agentstack/tools/~README.md index 9fedae09..ff5c6f07 100644 --- a/agentstack/tools/~README.md +++ b/agentstack/tools/~README.md @@ -1,5 +1,30 @@ -Tool Configuration Files -======================== + + +Tool Implementations +==================== +Each tool gets a directory in this folder. + +The directory should contain the following files: + +`config.json` +------------- +This contains the configuration for the tool for use by agentstack. + +`pyproject.toml` +---------------- +This contains the build configuration for the tool and any dependencies. + +`crew.py` +--------- +Python package which contains the tool implementation specific to CrewAI. +Use relative imports to include shared code from the root of the tool's package. +Deploy/install command should handle both single files and directories. + +Additional frameworks will get their own directories. + + +`config.json` Format +-------------------- Tools are configured for installation & removal using JSON files in this directory. ## Parameters @@ -22,10 +47,6 @@ Definitions for environment variables that will be appended to the local `.env` This is a list of key-value pairs ie. `[{"ENV_VAR": "value"}, ...]`. In cases where the user is expected to provide a value, the value is `"..."`. -### `packages` (list) [optional] -A list of package names to install. These are the names of the packages that will -be installed and removed by the package manager. - ### `post_install` (string) [optional] Shell command that will be executed after packages have been installed and environment variables have been set. diff --git a/tests/test_tool_config.py b/tests/test_tool_config.py index b1a3b789..21672c7e 100644 --- a/tests/test_tool_config.py +++ b/tests/test_tool_config.py @@ -41,7 +41,7 @@ def test_all_json_configs_from_tool_name(self): def test_all_json_configs_from_tool_path(self): for path in get_all_tool_paths(): try: - config = ToolConfig.from_json(path) + config = ToolConfig.from_json(path/'config.json') 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") From b85f0892d74c6d746ca7672294a9ff4f0af9e154 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Wed, 18 Dec 2024 14:35:36 -0800 Subject: [PATCH 07/34] Tools now get wrapped in a framework-specific layer to allow for our implementations to remain completely generic. Reimplemented some tools to work with all frameworks instead of just importing from crewai-tools. Removed `tools_bundled` from ToolCOnfig as all tools are now bundled for namespacing. `agentstack.tools` is not part of the public API and is to be used inside a user project to access tools. --- agentstack/__init__.py | 17 +++ agentstack/frameworks/__init__.py | 14 ++- agentstack/frameworks/crewai.py | 109 +++++++++++----- agentstack/generation/tool_generation.py | 102 ++------------- .../src/crew.py | 2 +- agentstack/tools.py | 71 ----------- agentstack/tools/__init__.py | 118 ++++++++++++++++++ agentstack/tools/agent_connect/__init__.py | 101 +++++++++++++++ agentstack/tools/agent_connect/common.py | 58 --------- agentstack/tools/agent_connect/config.json | 11 +- agentstack/tools/agent_connect/crewai.py | 41 ------ agentstack/tools/agent_connect/pyproject.toml | 10 -- agentstack/tools/browserbase/__init__.py | 30 +++++ agentstack/tools/browserbase/config.json | 9 +- agentstack/tools/browserbase/crewai.py | 3 - agentstack/tools/browserbase/pyproject.toml | 10 -- agentstack/tools/code_interpreter/Dockerfile | 6 + agentstack/tools/code_interpreter/__init__.py | 75 +++++++++++ agentstack/tools/code_interpreter/config.json | 9 +- agentstack/tools/code_interpreter/crewai.py | 3 - .../tools/code_interpreter/pyproject.toml | 8 -- agentstack/tools/directory_search/crewai.py | 2 +- agentstack/tools/exa/__init__.py | 32 +++++ agentstack/tools/exa/common.py | 32 ----- agentstack/tools/exa/config.json | 5 +- agentstack/tools/exa/crewai.py | 8 -- agentstack/tools/exa/pyproject.toml | 10 -- agentstack/tools/file_read/crewai.py | 2 +- agentstack/tools/firecrawl/__init__.py | 40 ++++++ agentstack/tools/firecrawl/common.py | 49 -------- agentstack/tools/firecrawl/config.json | 3 + agentstack/tools/firecrawl/crewai.py | 22 ---- agentstack/tools/firecrawl/pyproject.toml | 10 -- agentstack/tools/ftp/__init__.py | 64 ++++++++++ agentstack/tools/ftp/common.py | 38 ------ agentstack/tools/ftp/config.json | 6 +- agentstack/tools/ftp/crewai.py | 5 - agentstack/tools/ftp/pyproject.toml | 8 -- agentstack/tools/mem0/__init__.py | 37 ++++++ agentstack/tools/mem0/common.py | 36 ------ agentstack/tools/mem0/config.json | 5 +- agentstack/tools/mem0/crewai.py | 5 - agentstack/tools/mem0/pyproject.toml | 10 -- agentstack/tools/neon/__init__.py | 78 ++++++++++++ agentstack/tools/neon/common.py | 78 ------------ agentstack/tools/neon/config.json | 4 + agentstack/tools/neon/crewai.py | 6 - agentstack/tools/neon/pyproject.toml | 11 -- agentstack/tools/open_interpreter/__init__.py | 13 ++ agentstack/tools/open_interpreter/common.py | 12 -- agentstack/tools/open_interpreter/config.json | 6 + agentstack/tools/open_interpreter/crewai.py | 4 - .../tools/open_interpreter/pyproject.toml | 10 -- agentstack/tools/perplexity/__init__.py | 40 ++++++ agentstack/tools/perplexity/common.py | 49 -------- agentstack/tools/perplexity/config.json | 5 +- agentstack/tools/perplexity/crewai.py | 4 - agentstack/tools/perplexity/pyproject.toml | 10 -- agentstack/tools/stripe/crewai.py | 21 +--- pyproject.toml | 4 +- tests/fixtures/tool_config_max.json | 5 +- tests/test_frameworks.py | 40 ++---- tests/test_generation_tool.py | 13 +- tests/test_tool_config.py | 4 +- tests/test_tool_generation_init.py | 82 ------------ tox.ini | 1 + 66 files changed, 831 insertions(+), 905 deletions(-) delete mode 100644 agentstack/tools.py create mode 100644 agentstack/tools/__init__.py delete mode 100644 agentstack/tools/agent_connect/common.py delete mode 100644 agentstack/tools/agent_connect/crewai.py delete mode 100644 agentstack/tools/agent_connect/pyproject.toml delete mode 100644 agentstack/tools/browserbase/crewai.py delete mode 100644 agentstack/tools/browserbase/pyproject.toml create mode 100644 agentstack/tools/code_interpreter/Dockerfile delete mode 100644 agentstack/tools/code_interpreter/crewai.py delete mode 100644 agentstack/tools/code_interpreter/pyproject.toml delete mode 100644 agentstack/tools/exa/common.py delete mode 100644 agentstack/tools/exa/crewai.py delete mode 100644 agentstack/tools/exa/pyproject.toml delete mode 100644 agentstack/tools/firecrawl/common.py delete mode 100644 agentstack/tools/firecrawl/crewai.py delete mode 100644 agentstack/tools/firecrawl/pyproject.toml delete mode 100644 agentstack/tools/ftp/common.py delete mode 100644 agentstack/tools/ftp/crewai.py delete mode 100644 agentstack/tools/ftp/pyproject.toml delete mode 100644 agentstack/tools/mem0/common.py delete mode 100644 agentstack/tools/mem0/crewai.py delete mode 100644 agentstack/tools/mem0/pyproject.toml delete mode 100644 agentstack/tools/neon/common.py delete mode 100644 agentstack/tools/neon/crewai.py delete mode 100644 agentstack/tools/neon/pyproject.toml delete mode 100644 agentstack/tools/open_interpreter/common.py delete mode 100644 agentstack/tools/open_interpreter/crewai.py delete mode 100644 agentstack/tools/open_interpreter/pyproject.toml delete mode 100644 agentstack/tools/perplexity/common.py delete mode 100644 agentstack/tools/perplexity/crewai.py delete mode 100644 agentstack/tools/perplexity/pyproject.toml delete mode 100644 tests/test_tool_generation_init.py diff --git a/agentstack/__init__.py b/agentstack/__init__.py index e8addfb9..ca4e0a68 100644 --- a/agentstack/__init__.py +++ b/agentstack/__init__.py @@ -4,13 +4,16 @@ Methods that have been imported into this file are expected to be used by the end user inside of their project. """ +from typing import Callable from pathlib import Path from agentstack import conf from agentstack.utils import get_framework from agentstack.inputs import get_inputs +from agentstack import frameworks ___all___ = [ "conf", + "tools", "get_tags", "get_framework", "get_inputs", @@ -23,3 +26,17 @@ def get_tags() -> list[str]: """ return ['agentstack', get_framework(), *conf.get_installed_tools()] + +class ToolLoader: + """ + Provides the public interface for accessing tools, wrapped in the + framework-specific callable format. + + Get a tool's callables by name with `agentstack.tools[tool_name]` + Include them in your agent's tool list with `tools = [*agentstack.tools[tool_name], ]` + """ + def __getitem__(self, tool_name: str) -> list[Callable]: + return frameworks.get_tool_callables(tool_name) + +tools = ToolLoader() + diff --git a/agentstack/frameworks/__init__.py b/agentstack/frameworks/__init__.py index 0da933ab..3659b621 100644 --- a/agentstack/frameworks/__init__.py +++ b/agentstack/frameworks/__init__.py @@ -1,4 +1,4 @@ -from typing import Optional, Protocol +from typing import Optional, Protocol, Callable from types import ModuleType from importlib import import_module from pathlib import Path @@ -48,6 +48,12 @@ def remove_tool(self, tool: ToolConfig, agent_name: str) -> None: """ ... + def get_tool_callables(self, tool_name: str) -> list[Callable]: + """ + Get a tool by name and return it as a list of framework-native callables. + """ + ... + def get_agent_names(self) -> list[str]: """ Get a list of agent names in the user's project. @@ -114,6 +120,12 @@ def remove_tool(tool: ToolConfig, agent_name: str): """ return get_framework_module(get_framework()).remove_tool(tool, agent_name) +def get_tool_callables(tool_name: str) -> list[Callable]: + """ + Get a tool by name and return it as a list of framework-native callables. + """ + return get_framework_module(get_framework()).get_tool_callables(tool_name) + def get_agent_names() -> list[str]: """ Get a list of agent names in the user's project. diff --git a/agentstack/frameworks/crewai.py b/agentstack/frameworks/crewai.py index 69d21fb7..8ace0123 100644 --- a/agentstack/frameworks/crewai.py +++ b/agentstack/frameworks/crewai.py @@ -1,4 +1,4 @@ -from typing import Optional, Any +from typing import Optional, Any, Callable from pathlib import Path import ast from agentstack import conf @@ -8,6 +8,10 @@ from agentstack.agents import AgentConfig from agentstack.generation import asttools +try: + from crewai.tools import tool as _crewai_tool_decorator +except ImportError: + raise ValidationError("Could not import `crewai`. Is this an AgentStack CrewAI project?") ENTRYPOINT: Path = Path('src/crew.py') @@ -140,6 +144,44 @@ def get_agent_tools(self, agent_name: str) -> ast.List: return tools_kwarg.value + def get_agent_tool_nodes(self, agent_name: str) -> list[ast.Starred]: + """ + Get a list of all ast nodes that define agentstack tools used by the agent. + """ + tool_nodes: list[ast.Starred] = [] + agent_tools_node = self.get_agent_tools(agent_name) + for node in agent_tools_node.elts: + try: + # we need to find nodes that look like: + # `*agentstack.tools['tool_name']` + assert isinstance(node, ast.Starred) + assert isinstance(node.value, ast.Subscript) + assert isinstance(node.value.slice, ast.Constant) + name_node = node.value.value + assert isinstance(name_node, ast.Attribute) + assert isinstance(name_node.value, ast.Name) + assert name_node.value.id == 'agentstack' + assert name_node.attr == 'tools' + + # This is a starred subscript node referencing agentstack.tools with + # a string slice, so it must be an agentstack tool + tool_nodes.append(node) + except AssertionError: + continue # not a matched node; that's ok + return tool_nodes + + def get_agent_tool_names(self, agent_name: str) -> list[str]: + """ + Get a list of all tools used by the agent. + + Tools are identified by the item name of an `agentstack.tools` attribute node. + """ + tool_names: list[str] = [] + for node in self.get_agent_tool_nodes(agent_name): + # ignore type checking here since `get_agent_tool_nodes` is exhaustive + tool_names.append(node.value.slice.value) # type: ignore[attr-defined] + return tool_names + def add_agent_tools(self, agent_name: str, tool: ToolConfig): """ Add new tools to be used by an agent. @@ -156,21 +198,17 @@ def add_agent_tools(self, agent_name: str, tool: ToolConfig): existing_elts: list[ast.expr] = existing_node.elts new_tool_nodes: list[ast.expr] = [] - for tool_name in tool.tools: - # TODO there is definitely a better way to do this. We can't use - # a `set` becasue the ast nodes are unique objects. - _found = False - for elt in existing_elts: - if str(asttools.get_node_value(elt)) == tool_name: - _found = True - break # skip if the tool is already in the list - - if not _found: - # This prefixes the tool name with the 'tools' module - node: ast.expr = asttools.create_attribute('tools', tool_name) - if tool.tools_bundled: # Splat the variable if it's bundled - node = ast.Starred(value=node, ctx=ast.Load()) - existing_elts.append(node) + if not tool.name in self.get_agent_tool_names(agent_name): + # we need to create a node that looks like: + # `*agentstack.tools['tool_name']` + # we always get a list of callables from the `agentstack.tools` module, + # so we need to wrap the node in a `Starred` node to unpack it. + node = ast.Subscript( + value=asttools.create_attribute('agentstack', 'tools'), + slice=ast.Constant(tool.name), + ctx=ast.Load(), + ) + existing_elts.append(ast.Starred(value=node, ctx=ast.Load())) new_node = ast.List(elts=existing_elts, ctx=ast.Load()) start, end = self.get_node_range(existing_node) @@ -184,19 +222,12 @@ def remove_agent_tools(self, agent_name: str, tool: ToolConfig): start, end = self.get_node_range(existing_node) # modify the existing node to remove any matching tools - for tool_name in tool.tools: - for node in existing_node.elts: - if isinstance(node, ast.Starred): - if isinstance(node.value, ast.Attribute): - attr_name = node.value.attr - else: - continue # not an attribute node - elif isinstance(node, ast.Attribute): - attr_name = node.attr - else: - continue # not an attribute node - if attr_name == tool_name: - existing_node.elts.remove(node) + # we're referencing the internal node list from two directions here, + # so it's important that the node tree doesn't get re-parsed in between + for node in self.get_agent_tool_nodes(agent_name): + # ignore type checking here since `get_agent_tool_nodes` is exhaustive + if tool.name == node.value.slice.value: # type: ignore[attr-defined] + existing_node.elts.remove(node) self.edit_node_range(start, end, existing_node) @@ -267,8 +298,7 @@ def get_agent_tool_names(agent_name: str) -> list[Any]: Get a list of tools used by an agent. """ with CrewFile(conf.PATH / ENTRYPOINT) as crew_file: - tools = crew_file.get_agent_tools(agent_name) - return [asttools.get_node_value(node) for node in tools.elts] + return crew_file.get_agent_tool_names(agent_name) def add_agent(agent: AgentConfig) -> None: @@ -294,3 +324,20 @@ def remove_tool(tool: ToolConfig, agent_name: str): """ with CrewFile(conf.PATH / ENTRYPOINT) as crew_file: crew_file.remove_agent_tools(agent_name, tool) + + +def get_tool_callables(tool_name: str) -> list[Callable]: + """ + Get a tool implementations for use directly by a CrewAI agent. + """ + tool_funcs = [] + tool_config = ToolConfig.from_tool_name(tool_name) + for tool_func_name in tool_config.tools: + tool_func = getattr(tool_config.module, tool_func_name) + + assert callable(tool_func), f"Tool function {tool_func_name} is not callable." + assert tool_func.__doc__, f"Tool function {tool_func_name} is missing a docstring." + + # apply the CrewAI tool decorator to the tool function + tool_funcs.append(_crewai_tool_decorator(tool_func)) + return tool_funcs diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 314ff481..69ee7661 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -1,9 +1,5 @@ -import os -import sys +import os, sys from typing import Optional -from pathlib import Path -import shutil -import ast from agentstack import conf from agentstack.conf import ConfigFile @@ -12,72 +8,9 @@ from agentstack import packaging from agentstack.utils import term_color from agentstack.tools import ToolConfig -from agentstack.generation import asttools from agentstack.generation.files import EnvFile -# This is the filename of the location of tool imports in the user's project. -TOOLS_INIT_FILENAME: Path = Path("src/tools/__init__.py") - - -class ToolsInitFile(asttools.File): - """ - Modifiable AST representation of the tools init file. - - Use it as a context manager to make and save edits: - ```python - with ToolsInitFile(filename) as tools_init: - tools_init.add_import_for_tool(...) - ``` - """ - - def get_import_for_tool(self, tool: ToolConfig) -> Optional[ast.ImportFrom]: - """ - Get the import statement for a tool. - raises a ValidationError if the tool is imported multiple times. - """ - all_imports = asttools.get_all_imports(self.tree) - tool_imports = [i for i in all_imports if tool.module_name == i.module] - - if len(tool_imports) > 1: - raise ValidationError(f"Multiple imports for tool {tool.name} found in {self.filename}") - - try: - return tool_imports[0] - except IndexError: - return None - - def add_import_for_tool(self, tool: ToolConfig, framework: str): - """ - Add an import for a tool. - raises a ValidationError if the tool is already imported. - """ - tool_import = self.get_import_for_tool(tool) - if tool_import: - raise ValidationError(f"Tool {tool.name} already imported in {self.filename}") - - try: - last_import = asttools.get_all_imports(self.tree)[-1] - start, end = self.get_node_range(last_import) - except IndexError: - start, end = 0, 0 # No imports in the file - - import_statement = tool.get_import_statement(framework) - self.edit_node_range(end, end, f"\n{import_statement}") - - def remove_import_for_tool(self, tool: ToolConfig, framework: str): - """ - Remove an import for a tool. - raises a ValidationError if the tool is not imported. - """ - tool_import = self.get_import_for_tool(tool) - if not tool_import: - raise ValidationError(f"Tool {tool.name} not imported in {self.filename}") - - start, end = self.get_node_range(tool_import) - self.edit_node_range(start, end, "") - - def add_tool(tool_name: str, agents: Optional[list[str]] = []): agentstack_config = ConfigFile() tool = ToolConfig.from_tool_name(tool_name) @@ -85,19 +18,8 @@ def add_tool(tool_name: str, agents: Optional[list[str]] = []): if tool_name in agentstack_config.tools: print(term_color(f'Tool {tool_name} is already installed', 'blue')) else: # handle install - tool_file_path = tool.get_impl_file_path(agentstack_config.framework) - - if tool.packages: - packaging.install(' '.join(tool.packages)) - - # Move tool from package to project - shutil.copy(tool_file_path, conf.PATH / f'src/tools/{tool.module_name}.py') - - try: # Edit the user's project tool init file to include the tool - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.add_import_for_tool(tool, agentstack_config.framework) - except ValidationError as e: - print(term_color(f"Error adding tool:\n{e}", 'red')) + if tool.dependencies: + packaging.install(' '.join(tool.dependencies)) if tool.env: # add environment variables which don't exist with EnvFile() as env: @@ -132,21 +54,11 @@ def remove_tool(tool_name: str, agents: Optional[list[str]] = []): print(term_color(f'Tool {tool_name} is not installed', 'red')) sys.exit(1) + # TODO ensure other agents are not using the tool tool = ToolConfig.from_tool_name(tool_name) - if tool.packages: - packaging.remove(' '.join(tool.packages)) - - # TODO ensure that other agents in the project are not using the tool. - try: - os.remove(conf.PATH / f'src/tools/{tool.module_name}.py') - except FileNotFoundError: - print(f'"src/tools/{tool.module_name}.py" not found') - - try: # Edit the user's project tool init file to exclude the tool - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.remove_import_for_tool(tool, agentstack_config.framework) - except ValidationError as e: - print(term_color(f"Error removing tool:\n{e}", 'red')) + if tool.dependencies: + # TODO split on "==", ">=", etc. and only remove by package name + packaging.remove(' '.join(tool.dependencies)) # Edit the framework entrypoint file to exclude the tool in the agent definition if not agents: # If no agents are specified, remove the tool from all agents diff --git a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/crew.py b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/crew.py index 94a74a50..e8e224d8 100644 --- a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/crew.py +++ b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/crew.py @@ -1,6 +1,6 @@ from crewai import Agent, Crew, Process, Task from crewai.project import CrewBase, agent, crew, task -import tools +import agentstack @CrewBase class {{cookiecutter.project_metadata.project_name|replace('-', '')|replace('_', '')|capitalize}}Crew(): diff --git a/agentstack/tools.py b/agentstack/tools.py deleted file mode 100644 index 1acb8d97..00000000 --- a/agentstack/tools.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Optional -import os -import sys -from pathlib import Path -import pydantic -from agentstack.utils import get_package_path, open_json_file, term_color - - -class ToolConfig(pydantic.BaseModel): - """ - This represents the configuration data for a tool. - It parses and validates the `config.json` file for a tool. - """ - - name: str - category: str - tools: list[str] - url: Optional[str] = None - tools_bundled: bool = False - cta: Optional[str] = None - env: Optional[dict] = None - 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' - if not os.path.exists(path): # TODO raise exceptions and handle message/exit in cli - print(term_color(f'No known agentstack tool: {name}', 'red')) - sys.exit(1) - return cls.from_json(path) - - @classmethod - def from_json(cls, path: Path) -> 'ToolConfig': - data = open_json_file(path) - try: - return cls(**data) - except pydantic.ValidationError as e: - # TODO raise exceptions and handle message/exit in cli - print(term_color(f"Error validating tool config JSON: \n{path}", 'red')) - for error in e.errors(): - print(f"{' '.join([str(loc) for loc in error['loc']])}: {error['msg']}") - sys.exit(1) - - @property - def module_name(self) -> str: - return f"{self.name}_tool" - - def get_import_statement(self, framework: str) -> str: - return f"from .{self.module_name} import {', '.join(self.tools)}" - - def get_impl_file_path(self, framework: str) -> Path: - return get_package_path() / f'templates/{framework}/tools/{self.module_name}.py' - - -def get_all_tool_paths() -> list[Path]: - paths = [] - tools_dir = get_package_path() / 'tools' - for file in tools_dir.iterdir(): - if file.is_file() and file.suffix == '.json': - 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()] diff --git a/agentstack/tools/__init__.py b/agentstack/tools/__init__.py new file mode 100644 index 00000000..4783fdb4 --- /dev/null +++ b/agentstack/tools/__init__.py @@ -0,0 +1,118 @@ +from typing import Optional, Protocol, runtime_checkable +from types import ModuleType +import os +import sys +from pathlib import Path +from importlib import import_module +import pydantic +from agentstack.exceptions import ValidationError +from agentstack.utils import get_package_path, open_json_file, term_color, snake_to_camel + + +class ToolConfig(pydantic.BaseModel): + """ + This represents the configuration data for a tool. + It parses and validates the `config.json` file and provides a dynamic + interface for interacting with the tool implementation. + """ + + name: str + category: str + tools: list[str] + url: Optional[str] = None + cta: Optional[str] = None + env: Optional[dict] = None + dependencies: 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}/config.json' + if not os.path.exists(path): # TODO raise exceptions and handle message/exit in cli + print(term_color(f'No known agentstack tool: {name}', 'red')) + sys.exit(1) + return cls.from_json(path) + + @classmethod + def from_json(cls, path: Path) -> 'ToolConfig': + data = open_json_file(path) + try: + return cls(**data) + except pydantic.ValidationError as e: + # TODO raise exceptions and handle message/exit in cli + print(term_color(f"Error validating tool config JSON: \n{path}", 'red')) + for error in e.errors(): + print(f"{' '.join([str(loc) for loc in error['loc']])}: {error['msg']}") + sys.exit(1) + + @property + def type(self) -> type: + """ + Dynamically generate a type for the tool module. + ie. indicate what methods it's importable module should have. + """ + def method_stub(name: str): + def not_implemented(*args, **kwargs): + raise NotImplementedError(( + f"Method '{name}' is configured in config.json for tool '{self.name}'" + f"but has not been implemented in the tool module ({self.module_name})." + )) + return not_implemented + + type_ = type(f'{snake_to_camel(self.name)}Module', (Protocol,), { # type: ignore[arg-type] + method_name: method_stub(method_name) for method_name in self.tools + }) + return runtime_checkable(type_) + + @property + def module_name(self) -> str: + """Module name for the tool module.""" + return f"agentstack.tools.{self.name}" + + @property + def module(self) -> ModuleType: + """ + Import the tool module and validate that it implements the required methods. + Returns the imported module ready for direct use. + """ + try: + _module = import_module(self.module_name) + assert isinstance(_module, self.type) + return _module + except AssertionError as e: + raise ValidationError(( + f"Tool module `{self.module_name}` does not match the expected implementation. \n" + f"The tool's config.json file lists the following public methods: `{'`, `'.join(self.tools)}` " + f"but only implements: '{'`, `'.join([m for m in dir(_module) if not m.startswith('_')])}`" + )) + except ModuleNotFoundError as e: + raise ValidationError(( + f"Could not import tool module: {self.module_name}\n" + f"Are you sure you have installed the tool? (agentstack tools add {self.name})\n" + f"ModuleNotFoundError: {e}" + )) + + +def get_all_tool_paths() -> list[Path]: + """ + Get all the paths to the tool configuration files. + ie. agentstack/tools// + Tools are identified by having a `config.json` file instide the tools/ directory. + """ + paths = [] + tools_dir = get_package_path() / 'tools' + for tool_dir in tools_dir.iterdir(): + if tool_dir.is_dir(): + config_path = tool_dir / 'config.json' + if config_path.exists(): + paths.append(tool_dir) + 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_tool_name(path) for path in get_all_tool_names()] diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py index e69de29b..ff40bab5 100644 --- a/agentstack/tools/agent_connect/__init__.py +++ b/agentstack/tools/agent_connect/__init__.py @@ -0,0 +1,101 @@ +import os +import json +from agent_connect.simple_node import SimpleNode + + +# An HTTP and WS service will be started in agent-connect +# It can be an IP address or a domain name +host_domain = os.getenv("HOST_DOMAIN") +# Host port, default is 80 +host_port = os.getenv("HOST_PORT") +# WS path, default is /ws +host_ws_path = os.getenv("HOST_WS_PATH") +# Path to store DID document +did_document_path = os.getenv("DID_DOCUMENT_PATH") +# SSL certificate path, if using HTTPS, certificate and key need to be provided +ssl_cert_path = os.getenv("SSL_CERT_PATH") +ssl_key_path = os.getenv("SSL_KEY_PATH") + +if not host_domain: + raise Exception(( + "Host domain has not been provided.\n" + "Did you set the HOST_DOMAIN in you project's .env file?" + )) + +if not did_document_path: + raise Exception(( + "DID document path has not been provided.\n" + "Did you set the DID_DOCUMENT_PATH in you project's .env file?" + )) + + +def generate_did_info(node: SimpleNode, did_document_path: str) -> None: + """ + Generate or load DID information for a node. + + Args: + node: SimpleNode instance + did_document_path: Path to store/load DID document + """ + if os.path.exists(did_document_path): + print(f"Loading existing DID information from {did_document_path}") + with open(did_document_path, "r") as f: + did_info = json.load(f) + node.set_did_info(did_info["private_key_pem"], did_info["did"], did_info["did_document_json"]) + else: + print("Generating new DID information") + private_key_pem, did, did_document_json = node.generate_did_document() + node.set_did_info(private_key_pem, did, did_document_json) + + # Save DID information + if os.path.dirname(did_document_path): # allow saving to current directory + os.makedirs(os.path.dirname(did_document_path), exist_ok=True) + with open(did_document_path, "w") as f: + json.dump( + {"private_key_pem": private_key_pem, "did": did, "did_document_json": did_document_json}, + f, + indent=2, + ) + print(f"DID information saved to {did_document_path}") + + +agent_connect_simple_node = SimpleNode(host_domain, host_port, host_ws_path) +generate_did_info(agent_connect_simple_node, did_document_path) +agent_connect_simple_node.run() + + +async def send_message(message: str, destination_did: str) -> bool: + """ + Send a message through agent-connect node. + + Args: + message: Message content to be sent + destination_did: DID of the recipient agent + Returns: + bool: True if message was sent successfully, False otherwise + """ + try: + await agent_connect_simple_node.send_message(message, destination_did) + print(f"Successfully sent message: {message}") + return True + except Exception as e: + print(f"Failed to send message: {e}") + return False + + +async def receive_message() -> tuple[str, str]: + """ + Receive message from agent-connect node. + + Returns: + tuple[str, str]: Sender DID and received message content, empty string if no message or error occurred + """ + try: + sender_did, message = await agent_connect_simple_node.receive_message() + if message: + print(f"Received message from {sender_did}: {message}") + return sender_did, message + return "", "" + except Exception as e: + print(f"Failed to receive message: {e}") + return "", "" diff --git a/agentstack/tools/agent_connect/common.py b/agentstack/tools/agent_connect/common.py deleted file mode 100644 index 1dfd7e1c..00000000 --- a/agentstack/tools/agent_connect/common.py +++ /dev/null @@ -1,58 +0,0 @@ -from dotenv import load_dotenv -import os - -from agent_connect.simple_node import SimpleNode -import json - -load_dotenv() - -# An HTTP and WS service will be started in agent-connect -# It can be an IP address or a domain name -host_domain = os.getenv("HOST_DOMAIN") -# Host port, default is 80 -host_port = os.getenv("HOST_PORT") -# WS path, default is /ws -host_ws_path = os.getenv("HOST_WS_PATH") -# Path to store DID document -did_document_path = os.getenv("DID_DOCUMENT_PATH") -# SSL certificate path, if using HTTPS, certificate and key need to be provided -ssl_cert_path = os.getenv("SSL_CERT_PATH") -ssl_key_path = os.getenv("SSL_KEY_PATH") - - -def generate_did_info(node: SimpleNode, did_document_path: str) -> None: - """ - Generate or load DID information for a node. - - Args: - node: SimpleNode instance - did_document_path: Path to store/load DID document - """ - if os.path.exists(did_document_path): - print(f"Loading existing DID information from {did_document_path}") - with open(did_document_path, "r") as f: - did_info = json.load(f) - node.set_did_info( - did_info["private_key_pem"], - did_info["did"], - did_info["did_document_json"] - ) - else: - print("Generating new DID information") - private_key_pem, did, did_document_json = node.generate_did_document() - node.set_did_info(private_key_pem, did, did_document_json) - - # Save DID information - os.makedirs(os.path.dirname(did_document_path), exist_ok=True) - with open(did_document_path, "w") as f: - json.dump({ - "private_key_pem": private_key_pem, - "did": did, - "did_document_json": did_document_json - }, f, indent=2) - print(f"DID information saved to {did_document_path}") - -agent_connect_simple_node = SimpleNode(host_domain, host_port, host_ws_path) -generate_did_info(agent_connect_simple_node, did_document_path) -agent_connect_simple_node.run() - diff --git a/agentstack/tools/agent_connect/config.json b/agentstack/tools/agent_connect/config.json index eba04684..97e2c589 100644 --- a/agentstack/tools/agent_connect/config.json +++ b/agentstack/tools/agent_connect/config.json @@ -3,12 +3,15 @@ "url": "https://github.com/chgaowei/AgentConnect", "category": "network-protocols", "env": { - "HOST_DOMAIN": "...", + "HOST_DOMAIN": null, "HOST_PORT": 80, "HOST_WS_PATH": "/ws", - "DID_DOCUMENT_PATH": "...", - "SSL_CERT_PATH": "...", - "SSL_KEY_PATH": "..." + "DID_DOCUMENT_PATH": "data/agent_connect_did.json", + "SSL_CERT_PATH": null, + "SSL_KEY_PATH": null }, + "dependencies": [ + "agent-connect>=0.3.0" + ], "tools": ["send_message", "receive_message"] } diff --git a/agentstack/tools/agent_connect/crewai.py b/agentstack/tools/agent_connect/crewai.py deleted file mode 100644 index 74935199..00000000 --- a/agentstack/tools/agent_connect/crewai.py +++ /dev/null @@ -1,41 +0,0 @@ -from crewai_tools import tool -from .common import agent_connect_simple_node - - -@tool("Send Message to Agent by DID") -async def send_message(message: str, destination_did: str) -> bool: - """ - Send a message through agent-connect node. - - Args: - message: Message content to be sent - destination_did: DID of the recipient agent - Returns: - bool: True if message was sent successfully, False otherwise - """ - try: - await agent_connect_simple_node.send_message(message, destination_did) - print(f"Successfully sent message: {message}") - return True - except Exception as e: - print(f"Failed to send message: {e}") - return False - -@tool("Receive Message from Agent") -async def receive_message() -> tuple[str, str]: - """ - Receive message from agent-connect node. - - Returns: - tuple[str, str]: Sender DID and received message content, empty string if no message or error occurred - """ - try: - sender_did, message = await agent_connect_simple_node.receive_message() - if message: - print(f"Received message from {sender_did}: {message}") - return sender_did, message - return "", "" - except Exception as e: - print(f"Failed to receive message: {e}") - return "", "" - diff --git a/agentstack/tools/agent_connect/pyproject.toml b/agentstack/tools/agent_connect/pyproject.toml deleted file mode 100644 index 532b90c8..00000000 --- a/agentstack/tools/agent_connect/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.agent_connect" -version = "0.1" - -dependencies = [ - "agent-connect", -] - -[project.optional-dependencies] -crewai = [] \ No newline at end of file diff --git a/agentstack/tools/browserbase/__init__.py b/agentstack/tools/browserbase/__init__.py index e69de29b..6bd3425e 100644 --- a/agentstack/tools/browserbase/__init__.py +++ b/agentstack/tools/browserbase/__init__.py @@ -0,0 +1,30 @@ +import os +from typing import Optional, Any +from browserbase import Browserbase + + +BROWSERBASE_API_KEY = os.getenv("BROWSERBASE_API_KEY") +BROWSERBASE_PROJECT_ID = os.getenv("BROWSERBASE_PROJECT_ID") + +client = Browserbase(BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID) + + +# TODO can we define a type for the return value? +def load_url( + url: str, + text_content: Optional[bool] = True, + session_id: Optional[str] = None, + proxy: Optional[bool] = None, +) -> Any: + """ + Load a URL in a headless browser and return the page content. + + Args: + url: URL to load + text_content: Return text content if True, otherwise return raw content + session_id: Session ID to use for the browser + proxy: Use a proxy for the browser + Returns: + Any: Page content + """ + return client.load_url(url) diff --git a/agentstack/tools/browserbase/config.json b/agentstack/tools/browserbase/config.json index 1d3b83c2..01489d50 100644 --- a/agentstack/tools/browserbase/config.json +++ b/agentstack/tools/browserbase/config.json @@ -3,9 +3,12 @@ "url": "https://github.com/browserbase/python-sdk", "category": "browsing", "env": { - "BROWSERBASE_API_KEY": "...", - "BROWSERBASE_PROJECT_ID": "..." + "BROWSERBASE_API_KEY": null, + "BROWSERBASE_PROJECT_ID": null }, - "tools": ["browserbase"], + "dependencies": [ + "browserbase>=1.0.5" + ], + "tools": ["load_url"], "cta": "Create an API key at https://www.browserbase.com/" } \ No newline at end of file diff --git a/agentstack/tools/browserbase/crewai.py b/agentstack/tools/browserbase/crewai.py deleted file mode 100644 index d92f07a0..00000000 --- a/agentstack/tools/browserbase/crewai.py +++ /dev/null @@ -1,3 +0,0 @@ -from crewai_tools import BrowserbaseLoadTool - -browserbase = BrowserbaseLoadTool(text_content=True) \ No newline at end of file diff --git a/agentstack/tools/browserbase/pyproject.toml b/agentstack/tools/browserbase/pyproject.toml deleted file mode 100644 index 76f8a053..00000000 --- a/agentstack/tools/browserbase/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.browserbase" -version = "0.1" - -dependencies = [ - "browserbase", -] - -[project.optional-dependencies] -crewai = [] \ No newline at end of file diff --git a/agentstack/tools/code_interpreter/Dockerfile b/agentstack/tools/code_interpreter/Dockerfile new file mode 100644 index 00000000..680d690a --- /dev/null +++ b/agentstack/tools/code_interpreter/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-alpine + +RUN pip install requests beautifulsoup4 + +# Set the working directory +WORKDIR /workspace \ No newline at end of file diff --git a/agentstack/tools/code_interpreter/__init__.py b/agentstack/tools/code_interpreter/__init__.py index e69de29b..be82f1f7 100644 --- a/agentstack/tools/code_interpreter/__init__.py +++ b/agentstack/tools/code_interpreter/__init__.py @@ -0,0 +1,75 @@ +import os +from agentstack.utils import get_package_path +import docker + +CONTAINER_NAME = "code-interpreter" +DEFAULT_IMAGE_TAG = os.getenv("CODE_INTERPRETER_DEFAULT_IMAGE_TAG", "code-interpreter:latest") +DOCKERFILE_PATH = os.getenv("CODE_INTERPRETER_DOCKERFILE_PATH", get_package_path() / "tools/code_interpreter") + +client = docker.from_env() + + +def _verify_docker_image() -> None: + try: + client.images.get(DEFAULT_IMAGE_TAG) + except docker.errors.ImageNotFound: + if not os.path.exists(DOCKERFILE_PATH): + raise Exception(( + "Dockerfile path has not been provided.\n" + "Did you set the DOCKERFILE_PATH in you project's .env file?" + )) + + client.images.build( + path=DOCKERFILE_PATH, + tag=DEFAULT_IMAGE_TAG, + rm=True, + ) + + +def _init_docker_container() -> docker.models.containers.Container: + current_path = os.getcwd() + client = docker.from_env() + + # kill container if it's already running + try: + existing_container = client.containers.get(CONTAINER_NAME) + existing_container.stop() + existing_container.remove() + except docker.errors.NotFound: + pass + + return client.containers.run( + DEFAULT_IMAGE_TAG, + detach=True, + tty=True, + working_dir="/workspace", + name=CONTAINER_NAME, + volumes={current_path: {"bind": "/workspace", "mode": "rw"}}, # type: ignore + ) + + +def run_code(code: str, libraries_used: list[str]) -> str: + """ + Run the code in a Docker container using Python 3. + + The container will be built and started, the code will be executed, and the container will be stopped. + + Args: + code: The code to be executed. ALWAYS PRINT the final result and the output of the code. + libraries_used: A list of libraries to be installed in the container before running the code. + """ + _verify_docker_image() + container = _init_docker_container() + + for library in libraries_used: + container.exec_run(f"pip install {library}") + + result = container.exec_run(f'python3 -c "{code}"') + container.stop() + container.remove() + + return ( + f"exit code: {result.exit_code}\n" + f"{result.output.decode('utf-8')}" + ) + diff --git a/agentstack/tools/code_interpreter/config.json b/agentstack/tools/code_interpreter/config.json index d3de4a94..f164730f 100644 --- a/agentstack/tools/code_interpreter/config.json +++ b/agentstack/tools/code_interpreter/config.json @@ -1,5 +1,12 @@ { "name": "code_interpreter", "category": "code-execution", - "tools": ["code_interpreter"] + "env": { + "CODE_INTERPRETER_DEFAULT_IMAGE_TAG": null, + "CODE_INTERPRETER_DOCKERFILE_PATH": null + }, + "dependencies": [ + "docker>=7.1.0" + ], + "tools": ["run_code"] } \ No newline at end of file diff --git a/agentstack/tools/code_interpreter/crewai.py b/agentstack/tools/code_interpreter/crewai.py deleted file mode 100644 index f56f6497..00000000 --- a/agentstack/tools/code_interpreter/crewai.py +++ /dev/null @@ -1,3 +0,0 @@ -from crewai_tools import CodeInterpreterTool - -code_interpreter = CodeInterpreterTool() \ No newline at end of file diff --git a/agentstack/tools/code_interpreter/pyproject.toml b/agentstack/tools/code_interpreter/pyproject.toml deleted file mode 100644 index f630d2b1..00000000 --- a/agentstack/tools/code_interpreter/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "agentstack.code_interpreter" -version = "0.1" - -dependencies = [] - -[project.optional-dependencies] -crewai = [] \ No newline at end of file diff --git a/agentstack/tools/directory_search/crewai.py b/agentstack/tools/directory_search/crewai.py index ef15362a..d0d36710 100644 --- a/agentstack/tools/directory_search/crewai.py +++ b/agentstack/tools/directory_search/crewai.py @@ -22,4 +22,4 @@ # ) # ) -dir_search_tool = DirectorySearchTool() \ No newline at end of file +dir_search_tool = DirectorySearchTool() diff --git a/agentstack/tools/exa/__init__.py b/agentstack/tools/exa/__init__.py index e69de29b..19db5782 100644 --- a/agentstack/tools/exa/__init__.py +++ b/agentstack/tools/exa/__init__.py @@ -0,0 +1,32 @@ +import os +from exa_py import Exa + +# Check out our docs for more info! https://docs.exa.ai/ + +API_KEY = os.getenv('EXA_API_KEY') + + +def search_and_contents(question: str) -> str: + """ + Tool using Exa's Python SDK to run semantic search and return result highlights. + Args: + question: The search query or question to find information about + Returns: + Formatted string containing titles, URLs, and highlights from the search results + """ + exa = Exa(api_key=API_KEY) + + response = exa.search_and_contents( + question, type="neural", use_autoprompt=True, num_results=3, highlights=True + ) + + parsedResult = ''.join( + [ + f'{eachResult.title}' + f'{eachResult.url}' + f'{"".join(eachResult.highlights)}' + for (idx, eachResult) in enumerate(response.results) + ] + ) + + return parsedResult diff --git a/agentstack/tools/exa/common.py b/agentstack/tools/exa/common.py deleted file mode 100644 index 867055bb..00000000 --- a/agentstack/tools/exa/common.py +++ /dev/null @@ -1,32 +0,0 @@ -from exa_py import Exa -import os - - -# Check out our docs for more info! https://docs.exa.ai/ - -def search_and_contents(question: str) -> str: - """ - Tool using Exa's Python SDK to run semantic search and return result highlights. - Args: - question: The search query or question to find information about - Returns: - Formatted string containing titles, URLs, and highlights from the search results - """ - exa = Exa(api_key=os.getenv('EXA_API_KEY')) - - response = exa.search_and_contents( - question, - type="neural", - use_autoprompt=True, - num_results=3, - highlights=True - ) - - parsedResult = ''.join([ - f'{eachResult.title}' - f'{eachResult.url}' - f'{"".join(eachResult.highlights)}' - for (idx, eachResult) in enumerate(response.results) - ]) - - return parsedResult diff --git a/agentstack/tools/exa/config.json b/agentstack/tools/exa/config.json index 5ab36b4d..4f6a4fbd 100644 --- a/agentstack/tools/exa/config.json +++ b/agentstack/tools/exa/config.json @@ -3,8 +3,11 @@ "url": "https://exa.ai", "category": "web-retrieval", "env": { - "EXA_API_KEY": "..." + "EXA_API_KEY": null }, + "dependencies": [ + "exa-py>=1.7.0" + ], "tools": ["search_and_contents"], "cta": "Get your Exa API key at https://dashboard.exa.ai/api-keys" } \ No newline at end of file diff --git a/agentstack/tools/exa/crewai.py b/agentstack/tools/exa/crewai.py deleted file mode 100644 index d71e9462..00000000 --- a/agentstack/tools/exa/crewai.py +++ /dev/null @@ -1,8 +0,0 @@ -from crewai_tools import tool -from .common import search_and_contents as _search_and_contents - - -@tool("Exa search and get contents") -def search_and_contents(question: str) -> str: - return _search_and_contents(question) - diff --git a/agentstack/tools/exa/pyproject.toml b/agentstack/tools/exa/pyproject.toml deleted file mode 100644 index 03d7eb66..00000000 --- a/agentstack/tools/exa/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.exa" -version = "0.1" - -dependencies = [ - "exa_py" -] - -[project.optional-dependencies] -crewai = [] diff --git a/agentstack/tools/file_read/crewai.py b/agentstack/tools/file_read/crewai.py index 2f6b5bb2..5fa6cadd 100644 --- a/agentstack/tools/file_read/crewai.py +++ b/agentstack/tools/file_read/crewai.py @@ -1,3 +1,3 @@ from crewai_tools import FileReadTool -file_read_tool = FileReadTool() \ No newline at end of file +file_read_tool = FileReadTool() diff --git a/agentstack/tools/firecrawl/__init__.py b/agentstack/tools/firecrawl/__init__.py index e69de29b..1f912b31 100644 --- a/agentstack/tools/firecrawl/__init__.py +++ b/agentstack/tools/firecrawl/__init__.py @@ -0,0 +1,40 @@ +import os +from firecrawl import FirecrawlApp + +app = FirecrawlApp(api_key=os.getenv('FIRECRAWL_API_KEY')) + + +def web_scrape(url: str): + """ + Scrape a url and return markdown. Use this to read a singular page and web_crawl only if you + need to read all other links as well. + """ + scrape_result = app.scrape_url(url, params={'formats': ['markdown']}) + return scrape_result + + +def web_crawl(url: str): + """ + Scrape a url and crawl through other links from that page, scraping their contents. + This tool returns a crawl_id that you will need to use after waiting for a period of time + to retrieve the final contents. You should attempt to accomplish another task while waiting + for the crawl to complete. + + Crawl will ignore sublinks of a page if they aren’t children of the url you provide. + So, the website.com/other-parent/blog-1 wouldn’t be returned if you crawled website.com/blogs/. + """ + + crawl_status = app.crawl_url( + url, params={'limit': 100, 'scrapeOptions': {'formats': ['markdown']}}, poll_interval=30 + ) + + return crawl_status + + +def retrieve_web_crawl(crawl_id: str): + """ + Retrieve the results of a previously started web crawl. Crawls take time to process + so be sure to only use this tool some time after initiating a crawl. The result + will tell you if the crawl is finished. If it is not, wait some more time then try again. + """ + return app.check_crawl_status(crawl_id) diff --git a/agentstack/tools/firecrawl/common.py b/agentstack/tools/firecrawl/common.py deleted file mode 100644 index d1ed9268..00000000 --- a/agentstack/tools/firecrawl/common.py +++ /dev/null @@ -1,49 +0,0 @@ -from firecrawl import FirecrawlApp -import os - -app = FirecrawlApp(api_key=os.getenv('FIRECRAWL_API_KEY')) - - -@tool -def web_scrape(url: str): - """ - Scrape a url and return markdown. Use this to read a singular page and web_crawl only if you - need to read all other links as well. - """ - scrape_result = app.scrape_url(url, params={'formats': ['markdown']}) - return scrape_result - - -@tool -def web_crawl(url: str): - """ - Scrape a url and crawl through other links from that page, scraping their contents. - This tool returns a crawl_id that you will need to use after waiting for a period of time - to retrieve the final contents. You should attempt to accomplish another task while waiting - for the crawl to complete. - - Crawl will ignore sublinks of a page if they aren’t children of the url you provide. - So, the website.com/other-parent/blog-1 wouldn’t be returned if you crawled website.com/blogs/. - """ - - crawl_status = app.crawl_url( - url, - params={ - 'limit': 100, - 'scrapeOptions': {'formats': ['markdown']} - }, - poll_interval=30 - ) - - return crawl_status - - -@tool -def retrieve_web_crawl(crawl_id: str): - """ - Retrieve the results of a previously started web crawl. Crawls take time to process - so be sure to only use this tool some time after initiating a crawl. The result - will tell you if the crawl is finished. If it is not, wait some more time then try again. - """ - return app.check_crawl_status(crawl_id) - diff --git a/agentstack/tools/firecrawl/config.json b/agentstack/tools/firecrawl/config.json index 21f8aa20..5fdf100e 100644 --- a/agentstack/tools/firecrawl/config.json +++ b/agentstack/tools/firecrawl/config.json @@ -5,6 +5,9 @@ "env": { "FIRECRAWL_API_KEY": "..." }, + "dependencies": [ + "firecrawl-py>=1.6.4" + ], "tools": ["web_scrape", "web_crawl", "retrieve_web_crawl"], "cta": "Create an API key at https://www.firecrawl.dev/" } \ No newline at end of file diff --git a/agentstack/tools/firecrawl/crewai.py b/agentstack/tools/firecrawl/crewai.py deleted file mode 100644 index fb57dc01..00000000 --- a/agentstack/tools/firecrawl/crewai.py +++ /dev/null @@ -1,22 +0,0 @@ -from crewai_tools import tool -from .common import ( - web_scrape as _web_scrape, - web_crawl as _web_crawl, - retrieve_web_crawl as _retrieve_web_crawl, -) - - -@tool("Web Scrape") -def web_scrape(url: str): - return _web_scrape(url) - - -@tool("Web Crawl") -def web_crawl(url: str): - return _web_crawl(url) - - -@tool("Retrieve Web Crawl") -def retrieve_web_crawl(crawl_id: str): - return _retrieve_web_crawl(crawl_id) - diff --git a/agentstack/tools/firecrawl/pyproject.toml b/agentstack/tools/firecrawl/pyproject.toml deleted file mode 100644 index f4051627..00000000 --- a/agentstack/tools/firecrawl/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.firecrawl" -version = "0.1" - -dependencies = [ - "firecrawl-py", -] - -[project.optional-dependencies] -crewai = [] diff --git a/agentstack/tools/ftp/__init__.py b/agentstack/tools/ftp/__init__.py index e69de29b..889a6592 100644 --- a/agentstack/tools/ftp/__init__.py +++ b/agentstack/tools/ftp/__init__.py @@ -0,0 +1,64 @@ +import os +from ftplib import FTP + +HOST = os.getenv('FTP_HOST') +USER = os.getenv('FTP_USER') +PASSWORD = os.getenv("FTP_PASSWORD") +PATH = '/' + + +if not HOST: + raise Exception(( + "Host domain has not been provided.\n" + "Did you set the FTP_HOST in you project's .env file?" + )) + +if not USER: + raise Exception(( + "User has not been provided.\n" + "Did you set the FTP_USER in you project's .env file?" + )) + +if not PASSWORD: + raise Exception(( + "Password has not been provided.\n" + "Did you set the FTP_PASSWORD in you project's .env file?" + )) + + +def upload_files(file_paths: list[str]): + """ + Upload a list of files to the FTP server. + + Args: + file_paths: A list of file paths to upload to the FTP server. + Returns: + bool: True if all files were uploaded successfully, False otherwise. + """ + + assert HOST and USER and PASSWORD # appease type checker + + result = True + # Loop through each file path in the list + for file_path in file_paths: + # Establish FTP connection + with FTP(HOST) as ftp: + try: + # Login to the server + ftp.login(user=USER, passwd=PASSWORD) + print(f"Connected to FTP server: {HOST}") + + # Change to the desired directory on the server + ftp.cwd(PATH) + + # Open the file in binary mode for reading + with open(file_path, 'rb') as file: + # Upload the file + ftp.storbinary(f'STOR {file_path}', file) + print(f"Successfully uploaded {file_path} to {PATH}") + + except Exception as e: + print(f"An error occurred: {e}") + result = False + + return result diff --git a/agentstack/tools/ftp/common.py b/agentstack/tools/ftp/common.py deleted file mode 100644 index d09348c4..00000000 --- a/agentstack/tools/ftp/common.py +++ /dev/null @@ -1,38 +0,0 @@ -from ftplib import FTP -import os - - -# FTP server details -ftp_host = os.getenv('FTP_HOST') -ftp_user = os.getenv('FTP_USER') -ftp_p = os.getenv("FTP_PASSWORD") -ftp_path = '/' - - -def upload_files(file_paths: list[str]): - """Upload a list of files to the FTP server.""" - - result = True - # Loop through each file path in the list - for file_path in file_paths: - # Establish FTP connection - with FTP(ftp_host) as ftp: - try: - # Login to the server - ftp.login(user=ftp_user, passwd=ftp_p) - print(f"Connected to FTP server: {ftp_host}") - - # Change to the desired directory on the server - ftp.cwd(ftp_path) - - # Open the file in binary mode for reading - with open(file_path, 'rb') as file: - # Upload the file - ftp.storbinary(f'STOR {file_path}', file) - print(f"Successfully uploaded {file_path} to {ftp_path}") - - except Exception as e: - print(f"An error occurred: {e}") - result = False - - return result diff --git a/agentstack/tools/ftp/config.json b/agentstack/tools/ftp/config.json index b6dfc870..b60daa84 100644 --- a/agentstack/tools/ftp/config.json +++ b/agentstack/tools/ftp/config.json @@ -2,9 +2,9 @@ "name": "ftp", "category": "computer-control", "env": { - "FTP_HOST": "...", - "FTP_USER": "...", - "FTP_PASSWORD": "..." + "FTP_HOST": null, + "FTP_USER": null, + "FTP_PASSWORD": null }, "tools": ["upload_files"], "cta": "Be sure to add your FTP credentials to .env" diff --git a/agentstack/tools/ftp/crewai.py b/agentstack/tools/ftp/crewai.py deleted file mode 100644 index 0e4abc62..00000000 --- a/agentstack/tools/ftp/crewai.py +++ /dev/null @@ -1,5 +0,0 @@ -from crewai_tools import tool -from .commonn import upload_files - - -upload_files = tool("Upload Files to FTP Server")(upload_files) diff --git a/agentstack/tools/ftp/pyproject.toml b/agentstack/tools/ftp/pyproject.toml deleted file mode 100644 index 5f9df481..00000000 --- a/agentstack/tools/ftp/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "agentstack.ftp" -version = "0.1" - -dependencies = [] - -[project.optional-dependencies] -crewai = [] \ No newline at end of file diff --git a/agentstack/tools/mem0/__init__.py b/agentstack/tools/mem0/__init__.py index e69de29b..e969626a 100644 --- a/agentstack/tools/mem0/__init__.py +++ b/agentstack/tools/mem0/__init__.py @@ -0,0 +1,37 @@ +import os +import json +from mem0 import MemoryClient + +# These functions can be extended by changing the user_id parameter +# Memories are sorted by user_id + + +MEM0_API_KEY = os.getenv('MEM0_API_KEY') +client = MemoryClient(api_key=MEM0_API_KEY) + +# These tools will only save information about the user +# "Potato is a vegetable" is not a memory +# "My favorite food is potatoes" IS a memory + + +def write_to_memory(user_message: str) -> str: + """ + Writes data to the memory store for a user. The tool will decide what + specific information is important to store as memory. + """ + messages = [ + {"role": "user", "content": user_message}, + ] + result = client.add(messages, user_id='default') # configure user + return json.dumps(result) + + +def read_from_memory(query: str) -> str: + """ + Reads memories related to user based on a query. + """ + memories = client.search(query=query, user_id='default') + if memories: + return "\n".join([mem['memory'] for mem in memories]) + else: + return "No relevant memories found." diff --git a/agentstack/tools/mem0/common.py b/agentstack/tools/mem0/common.py deleted file mode 100644 index 3ffbe332..00000000 --- a/agentstack/tools/mem0/common.py +++ /dev/null @@ -1,36 +0,0 @@ -import json -from mem0 import MemoryClient -import os - -# These functions can be extended by changing the user_id parameter -# Memories are sorted by user_id - -MEM0_API_KEY = os.getenv('MEM0_API_KEY') -client = MemoryClient(api_key=MEM0_API_KEY) - -# These tools will only save information about the user -# "Potato is a vegetable" is not a memory -# "My favorite food is potatoes" IS a memory - - -def write_to_memory(user_message: str) -> str: - """ - Writes data to the memory store for a user. The tool will decide what - specific information is important to store as memory. - """ - messages = [ - {"role": "user", "content": user_message}, - ] - result = client.add(messages, user_id='default') # configure user - return json.dumps(result) - - -def read_from_memory(query: str) -> str: - """ - Reads memories related to user based on a query. - """ - memories = client.search(query=query, user_id='default') - if memories: - return "\n".join([mem['memory'] for mem in memories]) - else: - return "No relevant memories found." diff --git a/agentstack/tools/mem0/config.json b/agentstack/tools/mem0/config.json index fe71cc64..6ca85239 100644 --- a/agentstack/tools/mem0/config.json +++ b/agentstack/tools/mem0/config.json @@ -3,8 +3,11 @@ "url": "https://github.com/mem0ai/mem0", "category": "storage", "env": { - "MEM0_API_KEY": "..." + "MEM0_API_KEY": null }, + "dependencies": [ + "mem0ai>=0.1.35" + ], "tools": ["write_to_memory", "read_from_memory"], "cta": "Create your mem0 API key at https://mem0.ai/" } \ No newline at end of file diff --git a/agentstack/tools/mem0/crewai.py b/agentstack/tools/mem0/crewai.py deleted file mode 100644 index e10253ec..00000000 --- a/agentstack/tools/mem0/crewai.py +++ /dev/null @@ -1,5 +0,0 @@ -from crewai_tools import tool -from .common import write_to_memory, read_from_memory - -write_to_memory = tool("Write to Memory")(write_to_memory) -read_from_memory = tool("Read from Memory")(read_from_memory) diff --git a/agentstack/tools/mem0/pyproject.toml b/agentstack/tools/mem0/pyproject.toml deleted file mode 100644 index febb45d2..00000000 --- a/agentstack/tools/mem0/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.mem0" -version = "0.1" - -dependencies = [ - "mem0ai", -] - -[project.optional-dependencies] -crewai = [] \ No newline at end of file diff --git a/agentstack/tools/neon/__init__.py b/agentstack/tools/neon/__init__.py index e69de29b..e2430a1e 100644 --- a/agentstack/tools/neon/__init__.py +++ b/agentstack/tools/neon/__init__.py @@ -0,0 +1,78 @@ +import os +from neon_api import NeonAPI +import psycopg2 +from psycopg2.extras import RealDictCursor + + +NEON_API_KEY = os.getenv("NEON_API_KEY") +neon_client = NeonAPI(api_key=NEON_API_KEY) + + +def create_database(project_name: str) -> str: + """ + Creates a new Neon project. (this takes less than 500ms) + Args: + project_name: Name of the project to create + Returns: + the connection URI for the new project + """ + try: + project = neon_client.project_create(project={"name": project_name}).project + connection_uri = neon_client.connection_uri( + project_id=project.id, database_name="neondb", role_name="neondb_owner" + ).uri + return f"Project/database created, connection URI: {connection_uri}" + except Exception as e: + return f"Failed to create project: {str(e)}" + + +def execute_sql_ddl(connection_uri: str, command: str) -> str: + """ + Inserts data into a specified Neon database. + Args: + connection_uri: The connection URI for the Neon database + command: The DDL SQL command to execute + Returns: + the result of the DDL command + """ + conn = psycopg2.connect(connection_uri) + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + cur.execute(command) + conn.commit() + except Exception as e: + conn.rollback() + return f"Failed to execute DDL command: {str(e)}" + cur.close() + conn.close() + return f"Command succeeded" + + +def run_sql_query(connection_uri: str, query: str) -> str: + """ + Inserts data into a specified Neon database. + Args: + connection_uri: The connection URI for the Neon database + query: The SQL query to execute + Returns: + the result of the SQL query + """ + conn = psycopg2.connect(connection_uri) + cur = conn.cursor(cursor_factory=RealDictCursor) + try: + cur.execute(query) + conn.commit() + + # Try to fetch results (for SELECT queries) + try: + records = cur.fetchall() + return f"Query result: {records}" + except psycopg2.ProgrammingError: + # For INSERT/UPDATE/DELETE operations + return f"Query executed successfully" + except Exception as e: + conn.rollback() + return f"Failed to execute SQL query: {str(e)}" + finally: + cur.close() + conn.close() diff --git a/agentstack/tools/neon/common.py b/agentstack/tools/neon/common.py deleted file mode 100644 index e2430a1e..00000000 --- a/agentstack/tools/neon/common.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from neon_api import NeonAPI -import psycopg2 -from psycopg2.extras import RealDictCursor - - -NEON_API_KEY = os.getenv("NEON_API_KEY") -neon_client = NeonAPI(api_key=NEON_API_KEY) - - -def create_database(project_name: str) -> str: - """ - Creates a new Neon project. (this takes less than 500ms) - Args: - project_name: Name of the project to create - Returns: - the connection URI for the new project - """ - try: - project = neon_client.project_create(project={"name": project_name}).project - connection_uri = neon_client.connection_uri( - project_id=project.id, database_name="neondb", role_name="neondb_owner" - ).uri - return f"Project/database created, connection URI: {connection_uri}" - except Exception as e: - return f"Failed to create project: {str(e)}" - - -def execute_sql_ddl(connection_uri: str, command: str) -> str: - """ - Inserts data into a specified Neon database. - Args: - connection_uri: The connection URI for the Neon database - command: The DDL SQL command to execute - Returns: - the result of the DDL command - """ - conn = psycopg2.connect(connection_uri) - cur = conn.cursor(cursor_factory=RealDictCursor) - try: - cur.execute(command) - conn.commit() - except Exception as e: - conn.rollback() - return f"Failed to execute DDL command: {str(e)}" - cur.close() - conn.close() - return f"Command succeeded" - - -def run_sql_query(connection_uri: str, query: str) -> str: - """ - Inserts data into a specified Neon database. - Args: - connection_uri: The connection URI for the Neon database - query: The SQL query to execute - Returns: - the result of the SQL query - """ - conn = psycopg2.connect(connection_uri) - cur = conn.cursor(cursor_factory=RealDictCursor) - try: - cur.execute(query) - conn.commit() - - # Try to fetch results (for SELECT queries) - try: - records = cur.fetchall() - return f"Query result: {records}" - except psycopg2.ProgrammingError: - # For INSERT/UPDATE/DELETE operations - return f"Query executed successfully" - except Exception as e: - conn.rollback() - return f"Failed to execute SQL query: {str(e)}" - finally: - cur.close() - conn.close() diff --git a/agentstack/tools/neon/config.json b/agentstack/tools/neon/config.json index 193fc230..ec2c772b 100644 --- a/agentstack/tools/neon/config.json +++ b/agentstack/tools/neon/config.json @@ -5,6 +5,10 @@ "env": { "NEON_API_KEY": "..." }, + "dependencies": [ + "neon-api>=0.1.5", + "psycopg2-binary" + ], "tools": ["create_database", "execute_sql_ddl", "run_sql_query"], "cta": "Create an API key at https://www.neon.tech" } \ No newline at end of file diff --git a/agentstack/tools/neon/crewai.py b/agentstack/tools/neon/crewai.py deleted file mode 100644 index d70fa719..00000000 --- a/agentstack/tools/neon/crewai.py +++ /dev/null @@ -1,6 +0,0 @@ -from crewai_tools import tool -from .common import create_database, execute_sql_ddl, run_sql_query - -create_database = tool("Create Neon Project and Database")(create_database) -execute_sql_ddl = tool("Execute SQL DDL")(execute_sql_ddl) -run_sql_query = tool("Execute SQL DML")(run_sql_query) diff --git a/agentstack/tools/neon/pyproject.toml b/agentstack/tools/neon/pyproject.toml deleted file mode 100644 index eba069d3..00000000 --- a/agentstack/tools/neon/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "agentstack.neon" -version = "0.1" - -dependencies = [ - "neon-api", - "psycopg2-binary", -] - -[project.optional-dependencies] -crewai = [] diff --git a/agentstack/tools/open_interpreter/__init__.py b/agentstack/tools/open_interpreter/__init__.py index e69de29b..8d922d49 100644 --- a/agentstack/tools/open_interpreter/__init__.py +++ b/agentstack/tools/open_interpreter/__init__.py @@ -0,0 +1,13 @@ +import os +from interpreter import interpreter + + +# 1. Configuration and Tools +interpreter.auto_run = True +interpreter.llm.model = os.getenv("OPEN_INTERPRETER_LLM_MODEL") + + +def execute_code(code: str): + """A tool to execute code using Open Interpreter. Returns the output of the code.""" + result = interpreter.chat(f"execute this code with no changes: {code}") + return result diff --git a/agentstack/tools/open_interpreter/common.py b/agentstack/tools/open_interpreter/common.py deleted file mode 100644 index 8e8b9283..00000000 --- a/agentstack/tools/open_interpreter/common.py +++ /dev/null @@ -1,12 +0,0 @@ -from interpreter import interpreter - - -# 1. Configuration and Tools -interpreter.auto_run = True -interpreter.llm.model = "gpt-4o" - - -def execute_code(code: str): - """A tool to execute code using Open Interpreter. Returns the output of the code.""" - result = interpreter.chat(f"execute this code with no changes: {code}") - return result diff --git a/agentstack/tools/open_interpreter/config.json b/agentstack/tools/open_interpreter/config.json index 25c6abe0..1e7e93a6 100644 --- a/agentstack/tools/open_interpreter/config.json +++ b/agentstack/tools/open_interpreter/config.json @@ -2,5 +2,11 @@ "name": "open_interpreter", "url": "https://github.com/OpenInterpreter/open-interpreter", "category": "code-execution", + "env": { + "OPEN_INTERPRETER_LLM_MODEL": "gpt-4o" + }, + "dependencies": [ + "open-interpreter>=0.3.7" + ], "tools": ["execute_code"] } \ No newline at end of file diff --git a/agentstack/tools/open_interpreter/crewai.py b/agentstack/tools/open_interpreter/crewai.py deleted file mode 100644 index 8615b32d..00000000 --- a/agentstack/tools/open_interpreter/crewai.py +++ /dev/null @@ -1,4 +0,0 @@ -from crewai_tools import tool -from .common import execute_code - -execute_code = tool("Execute Code")(execute_code) diff --git a/agentstack/tools/open_interpreter/pyproject.toml b/agentstack/tools/open_interpreter/pyproject.toml deleted file mode 100644 index 79631f87..00000000 --- a/agentstack/tools/open_interpreter/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.open_interpreter" -version = "0.1" - -dependencies = [ - "open-interpreter", -] - -[project.optional-dependencies] -crewai = [] diff --git a/agentstack/tools/perplexity/__init__.py b/agentstack/tools/perplexity/__init__.py index e69de29b..6422a648 100644 --- a/agentstack/tools/perplexity/__init__.py +++ b/agentstack/tools/perplexity/__init__.py @@ -0,0 +1,40 @@ +import os +import requests + + +url = "https://api.perplexity.ai/chat/completions" +api_key = os.getenv("PERPLEXITY_API_KEY") + + +def query_perplexity(query: str): + """ + Use Perplexity to concisely search the internet and answer a query with up-to-date information. + """ + + payload = { + "model": "llama-3.1-sonar-small-128k-online", + "messages": [ + {"role": "system", "content": "Be precise and concise."}, + {"role": "user", "content": query}, + ], + # "max_tokens": "Optional", + "temperature": 0.2, + "top_p": 0.9, + "return_citations": True, + "search_domain_filter": ["perplexity.ai"], + "return_images": False, + "return_related_questions": False, + "search_recency_filter": "month", + "top_k": 0, + "stream": False, + "presence_penalty": 0, + "frequency_penalty": 1, + } + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + + response = requests.request("POST", url, json=payload, headers=headers) + if response.status_code == 200 and response.text: + return response.text + else: + print(f"{response.status_code} - {response.text}") + return "Failed to query perplexity" diff --git a/agentstack/tools/perplexity/common.py b/agentstack/tools/perplexity/common.py deleted file mode 100644 index f13d5d51..00000000 --- a/agentstack/tools/perplexity/common.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import requests - - -url = "https://api.perplexity.ai/chat/completions" -api_key = os.getenv("PERPLEXITY_API_KEY") - - -def query_perplexity(query: str): - """ - Use Perplexity to concisely search the internet and answer a query with up-to-date information. - """ - - payload = { - "model": "llama-3.1-sonar-small-128k-online", - "messages": [ - { - "role": "system", - "content": "Be precise and concise." - }, - { - "role": "user", - "content": query - } - ], - # "max_tokens": "Optional", - "temperature": 0.2, - "top_p": 0.9, - "return_citations": True, - "search_domain_filter": ["perplexity.ai"], - "return_images": False, - "return_related_questions": False, - "search_recency_filter": "month", - "top_k": 0, - "stream": False, - "presence_penalty": 0, - "frequency_penalty": 1 - } - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json" - } - - response = requests.request("POST", url, json=payload, headers=headers) - if response.status_code == 200 and response.text: - return response.text - else: - print(f"{response.status_code} - {response.text}") - return "Failed to query perplexity" diff --git a/agentstack/tools/perplexity/config.json b/agentstack/tools/perplexity/config.json index ba6fe696..43a80f45 100644 --- a/agentstack/tools/perplexity/config.json +++ b/agentstack/tools/perplexity/config.json @@ -3,7 +3,10 @@ "url": "https://perplexity.ai", "category": "search", "env": { - "PERPLEXITY_API_KEY": "..." + "PERPLEXITY_API_KEY": null }, + "dependencies": [ + "requests>=2.30" + ], "tools": ["query_perplexity"] } \ No newline at end of file diff --git a/agentstack/tools/perplexity/crewai.py b/agentstack/tools/perplexity/crewai.py deleted file mode 100644 index 598c92c0..00000000 --- a/agentstack/tools/perplexity/crewai.py +++ /dev/null @@ -1,4 +0,0 @@ -from crewai_tools import tool -from .common import query_perplexity - -query_perplexity = tool("Query Perplexity")(query_perplexity) diff --git a/agentstack/tools/perplexity/pyproject.toml b/agentstack/tools/perplexity/pyproject.toml deleted file mode 100644 index 1b34cd56..00000000 --- a/agentstack/tools/perplexity/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.perplexity" -version = "0.1" - -dependencies = [ - "open-interpreter", -] - -[project.optional-dependencies] -crewai = [] diff --git a/agentstack/tools/stripe/crewai.py b/agentstack/tools/stripe/crewai.py index 0a668e13..8da76619 100644 --- a/agentstack/tools/stripe/crewai.py +++ b/agentstack/tools/stripe/crewai.py @@ -1,3 +1,4 @@ +import os from stripe_agent_toolkit.crewai.toolkit import StripeAgentToolkit @@ -5,19 +6,9 @@ secret_key=os.getenv("STRIPE_SECRET_KEY"), configuration={ "actions": { - "payment_links": { - "create": True, - "read": True, - "update": False - }, - "products": { - "create": True, - "update": True - }, - "prices": { - "create": True, - "update": True - }, + "payment_links": {"create": True, "read": True, "update": False}, + "products": {"create": True, "update": True}, + "prices": {"create": True, "update": True}, } - }).get_tools() - + }, +).get_tools() diff --git a/pyproject.toml b/pyproject.toml index 69f3c301..343a13b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,9 @@ crewai = [ "crewai==0.83.0", "crewai-tools==0.14.0", ] - +all = [ + "agentstack[dev,test,crewai]", +] [tool.setuptools.package-data] agentstack = ["templates/**/*"] diff --git a/tests/fixtures/tool_config_max.json b/tests/fixtures/tool_config_max.json index 1e5734d1..1ec8b0fc 100644 --- a/tests/fixtures/tool_config_max.json +++ b/tests/fixtures/tool_config_max.json @@ -3,12 +3,15 @@ "category": "category", "tools": ["tool1", "tool2"], "url": "https://example.com", - "tools_bundled": true, "cta": "Click me!", "env": { "ENV_VAR1": "value1", "ENV_VAR2": "value2" }, + "dependencies": [ + "dependency1>=1.0.0", + "dependency2>=2.0.0" + ], "post_install": "install.sh", "post_remove": "remove.sh" } \ No newline at end of file diff --git a/tests/test_frameworks.py b/tests/test_frameworks.py index 4b8e3cf3..a469bdaf 100644 --- a/tests/test_frameworks.py +++ b/tests/test_frameworks.py @@ -15,9 +15,10 @@ @parameterized_class([{"framework": framework} for framework in frameworks.SUPPORTED_FRAMEWORKS]) class TestFrameworks(unittest.TestCase): def setUp(self): - self.project_dir = BASE_PATH / 'tmp' / self.framework + self.project_dir = BASE_PATH / 'tmp' / self.framework / 'test_frameworks' os.makedirs(self.project_dir) + os.chdir(self.project_dir) # importing the crewai module requires us to be in a working directory os.makedirs(self.project_dir / 'src') os.makedirs(self.project_dir / 'src' / 'tools') @@ -45,10 +46,8 @@ def _populate_max_entrypoint(self): def _get_test_tool(self) -> ToolConfig: return ToolConfig(name='test_tool', category='test', tools=['test_tool']) - def _get_test_tool_starred(self) -> ToolConfig: - return ToolConfig( - name='test_tool_star', category='test', tools=['test_tool_star'], tools_bundled=True - ) + def _get_test_tool_alternate(self) -> ToolConfig: + return ToolConfig(name='test_tool_alt', category='test', tools=['test_tool_alt']) def test_get_framework_module(self): module = frameworks.get_framework_module(self.framework) @@ -73,14 +72,7 @@ def test_add_tool(self): entrypoint_src = open(frameworks.get_entrypoint_path(self.framework)).read() # TODO these asserts are not framework agnostic - assert 'tools=[tools.test_tool' in entrypoint_src - - def test_add_tool_starred(self): - self._populate_max_entrypoint() - frameworks.add_tool(self._get_test_tool_starred(), 'test_agent') - - entrypoint_src = open(frameworks.get_entrypoint_path(self.framework)).read() - assert 'tools=[*tools.test_tool_star' in entrypoint_src + assert "tools=[*agentstack.tools['test_tool']" in entrypoint_src def test_add_tool_invalid(self): self._populate_min_entrypoint() @@ -93,33 +85,25 @@ def test_remove_tool(self): frameworks.remove_tool(self._get_test_tool(), 'test_agent') entrypoint_src = open(frameworks.get_entrypoint_path(self.framework)).read() - assert 'tools=[tools.test_tool' not in entrypoint_src - - def test_remove_tool_starred(self): - self._populate_max_entrypoint() - frameworks.add_tool(self._get_test_tool_starred(), 'test_agent') - frameworks.remove_tool(self._get_test_tool_starred(), 'test_agent') - - entrypoint_src = open(frameworks.get_entrypoint_path(self.framework)).read() - assert 'tools=[*tools.test_tool_star' not in entrypoint_src + assert "tools=[*agentstack.tools['test_tool']" not in entrypoint_src def test_add_multiple_tools(self): self._populate_max_entrypoint() frameworks.add_tool(self._get_test_tool(), 'test_agent') - frameworks.add_tool(self._get_test_tool_starred(), 'test_agent') + frameworks.add_tool(self._get_test_tool_alternate(), 'test_agent') entrypoint_src = open(frameworks.get_entrypoint_path(self.framework)).read() assert ( # ordering is not guaranteed - 'tools=[tools.test_tool, *tools.test_tool_star' in entrypoint_src - or 'tools=[*tools.test_tool_star, tools.test_tool' in entrypoint_src + "tools=[*agentstack.tools['test_tool'], *agentstack.tools['test_tool_alt']" in entrypoint_src + or "tools=[*agentstack.tools['test_tool_alt'], *agentstack.tools['test_tool']" in entrypoint_src ) def test_remove_one_tool_of_multiple(self): self._populate_max_entrypoint() frameworks.add_tool(self._get_test_tool(), 'test_agent') - frameworks.add_tool(self._get_test_tool_starred(), 'test_agent') + frameworks.add_tool(self._get_test_tool_alternate(), 'test_agent') frameworks.remove_tool(self._get_test_tool(), 'test_agent') entrypoint_src = open(frameworks.get_entrypoint_path(self.framework)).read() - assert 'tools=[tools.test_tool' not in entrypoint_src - assert 'tools=[*tools.test_tool_star' in entrypoint_src + assert "*agentstack.tools['test_tool']" not in entrypoint_src + assert "tools=[*agentstack.tools['test_tool_alt']" in entrypoint_src diff --git a/tests/test_generation_tool.py b/tests/test_generation_tool.py index 779f7f91..c1993389 100644 --- a/tests/test_generation_tool.py +++ b/tests/test_generation_tool.py @@ -8,7 +8,7 @@ from agentstack.conf import ConfigFile, set_path from agentstack import frameworks from agentstack.tools import get_all_tools, ToolConfig -from agentstack.generation.tool_generation import add_tool, remove_tool, TOOLS_INIT_FILENAME +from agentstack.generation.tool_generation import add_tool, remove_tool BASE_PATH = Path(__file__).parent @@ -24,7 +24,6 @@ def setUp(self): os.makedirs(self.project_dir / 'src') os.makedirs(self.project_dir / 'src' / 'tools') (self.project_dir / 'src' / '__init__.py').touch() - (self.project_dir / TOOLS_INIT_FILENAME).touch() # set the framework in agentstack.json shutil.copy(BASE_PATH / 'fixtures' / 'agentstack.json', self.project_dir / 'agentstack.json') @@ -45,13 +44,10 @@ def test_add_tool(self): entrypoint_path = frameworks.get_entrypoint_path(self.framework) entrypoint_src = open(entrypoint_path).read() - ast.parse(entrypoint_src) - tools_init_src = open(self.project_dir / TOOLS_INIT_FILENAME).read() + ast.parse(entrypoint_src) # validate syntax # TODO verify tool is added to all agents (this is covered in test_frameworks.py) # assert 'agent_connect' in entrypoint_src - assert f'from .{tool_conf.module_name} import' in tools_init_src - assert (self.project_dir / 'src' / 'tools' / f'{tool_conf.module_name}.py').exists() assert 'agent_connect' in open(self.project_dir / 'agentstack.json').read() def test_remove_tool(self): @@ -61,11 +57,8 @@ def test_remove_tool(self): entrypoint_path = frameworks.get_entrypoint_path(self.framework) entrypoint_src = open(entrypoint_path).read() - ast.parse(entrypoint_src) - tools_init_src = open(self.project_dir / TOOLS_INIT_FILENAME).read() + ast.parse(entrypoint_src) # validate syntax # TODO verify tool is removed from all agents (this is covered in test_frameworks.py) # assert 'agent_connect' not in entrypoint_src - assert f'from .{tool_conf.module_name} import' not in tools_init_src - assert not (self.project_dir / 'src' / 'tools' / f'{tool_conf.module_name}.py').exists() assert 'agent_connect' not in open(self.project_dir / 'agentstack.json').read() diff --git a/tests/test_tool_config.py b/tests/test_tool_config.py index ed9f4ae6..df8e06a6 100644 --- a/tests/test_tool_config.py +++ b/tests/test_tool_config.py @@ -13,7 +13,6 @@ def test_minimal_json(self): assert config.category == "category" assert config.tools == ["tool1", "tool2"] assert config.url is None - assert config.tools_bundled is False assert config.cta is None assert config.env is None assert config.post_install is None @@ -25,7 +24,6 @@ def test_maximal_json(self): assert config.category == "category" assert config.tools == ["tool1", "tool2"] assert config.url == "https://example.com" - assert config.tools_bundled is True assert config.cta == "Click me!" assert config.env == {"ENV_VAR1": "value1", "ENV_VAR2": "value2"} assert config.post_install == "install.sh" @@ -40,7 +38,7 @@ def test_all_json_configs_from_tool_name(self): def test_all_json_configs_from_tool_path(self): for path in get_all_tool_paths(): try: - config = ToolConfig.from_json(path) + config = ToolConfig.from_json(f"{path}/config.json") 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" diff --git a/tests/test_tool_generation_init.py b/tests/test_tool_generation_init.py deleted file mode 100644 index 8eb0b65a..00000000 --- a/tests/test_tool_generation_init.py +++ /dev/null @@ -1,82 +0,0 @@ -import os, sys -from pathlib import Path -import shutil -import unittest -from parameterized import parameterized_class - -from agentstack import conf -from agentstack.conf import ConfigFile -from agentstack.exceptions import ValidationError -from agentstack import frameworks -from agentstack.tools import ToolConfig -from agentstack.generation.tool_generation import ToolsInitFile, TOOLS_INIT_FILENAME - - -BASE_PATH = Path(__file__).parent - - -@parameterized_class([{"framework": framework} for framework in frameworks.SUPPORTED_FRAMEWORKS]) -class TestToolGenerationInit(unittest.TestCase): - def setUp(self): - self.project_dir = BASE_PATH / 'tmp' / 'tool_generation_init' - os.makedirs(self.project_dir) - os.makedirs(self.project_dir / 'src') - os.makedirs(self.project_dir / 'src' / 'tools') - (self.project_dir / 'src' / '__init__.py').touch() - (self.project_dir / 'src' / 'tools' / '__init__.py').touch() - shutil.copy(BASE_PATH / 'fixtures' / 'agentstack.json', self.project_dir / 'agentstack.json') - - conf.set_path(self.project_dir) - with ConfigFile() as config: - config.framework = self.framework - - def tearDown(self): - shutil.rmtree(self.project_dir) - - def _get_test_tool(self) -> ToolConfig: - return ToolConfig(name='test_tool', category='test', tools=['test_tool']) - - def _get_test_tool_alt(self) -> ToolConfig: - return ToolConfig(name='test_tool_alt', category='test', tools=['test_tool_alt']) - - def test_tools_init_file(self): - tools_init = ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) - # file is empty - assert tools_init.get_import_for_tool(self._get_test_tool()) == None - - def test_tools_init_file_missing(self): - with self.assertRaises(ValidationError) as context: - tools_init = ToolsInitFile(conf.PATH / 'missing') - - def test_tools_init_file_add_import(self): - tool = self._get_test_tool() - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.add_import_for_tool(tool, self.framework) - - tool_init_src = open(self.project_dir / TOOLS_INIT_FILENAME).read() - assert tool.get_import_statement(self.framework) in tool_init_src - - def test_tools_init_file_add_import_multiple(self): - tool = self._get_test_tool() - tool_alt = self._get_test_tool_alt() - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.add_import_for_tool(tool, self.framework) - - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.add_import_for_tool(tool_alt, self.framework) - - # Should not be able to re-add a tool import - with self.assertRaises(ValidationError) as context: - with ToolsInitFile(conf.PATH / TOOLS_INIT_FILENAME) as tools_init: - tools_init.add_import_for_tool(tool, self.framework) - - tool_init_src = open(self.project_dir / TOOLS_INIT_FILENAME).read() - assert tool.get_import_statement(self.framework) in tool_init_src - assert tool_alt.get_import_statement(self.framework) in tool_init_src - # TODO this might be a little too strict - assert ( - tool_init_src - == """ -from .test_tool_tool import test_tool -from .test_tool_alt_tool import test_tool_alt""" - ) diff --git a/tox.ini b/tox.ini index 6c6a966b..601b5123 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ envlist = py310,py311,py312 [testenv] +extras = all # install dependencies for all frameworks, too deps = pytest parameterized From dffff68f67fc90c434b0a03a03f01335dcde22c4 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Wed, 18 Dec 2024 15:30:22 -0800 Subject: [PATCH 08/34] EnvFile now prefixes empty variables with a comment (#) to avoid overriding existing variables. More robust stripping on parse, too. --- agentstack/generation/files.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/agentstack/generation/files.py b/agentstack/generation/files.py index f2ad90a0..fa218570 100644 --- a/agentstack/generation/files.py +++ b/agentstack/generation/files.py @@ -1,4 +1,5 @@ from typing import Optional, Union +import string import os, sys from pathlib import Path @@ -20,6 +21,10 @@ class EnvFile: 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. + + If the value of a variable is None, it will be commented out when it is written + to the file. This gives the user a suggestion, but doesn't override values that + may have been set by the user via other means. `path` is the directory where the .env file is located. Defaults to the current working directory. @@ -57,7 +62,7 @@ def append_if_new(self, key, value): def read(self): def parse_line(line): key, value = line.split('=') - return key.strip(), value.strip() + return key.strip(string.whitespace + '#'), value.strip(string.whitespace + '"') if os.path.exists(conf.PATH / self._filename): with open(conf.PATH / self._filename, 'r') as f: @@ -69,7 +74,10 @@ def parse_line(line): def write(self): with open(conf.PATH / self._filename, 'a') as f: for key, value in self._new_variables.items(): - f.write(f"\n{key}={value}") + if value is None: + f.write(f'\n#{key}=""') # comment-out empty values + else: + f.write(f'\n{key}={value}') def __enter__(self) -> 'EnvFile': return self From 27b78fcca8c0fe3889f06bde4b571fc71067b061 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:48:30 +0000 Subject: [PATCH 09/34] refactor: make directory_search tool framework-agnostic Co-Authored-By: root@a10k.co --- agentstack/tools/directory_search/__init__.py | 3 ++ agentstack/tools/directory_search/config.json | 9 +++-- .../directory_search/directory_search.py | 39 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 agentstack/tools/directory_search/directory_search.py diff --git a/agentstack/tools/directory_search/__init__.py b/agentstack/tools/directory_search/__init__.py index e69de29b..58155801 100644 --- a/agentstack/tools/directory_search/__init__.py +++ b/agentstack/tools/directory_search/__init__.py @@ -0,0 +1,3 @@ +from .directory_search import search_directory, search_fixed_directory + +__all__ = ['search_directory', 'search_fixed_directory'] diff --git a/agentstack/tools/directory_search/config.json b/agentstack/tools/directory_search/config.json index b412d03b..a8b83c8d 100644 --- a/agentstack/tools/directory_search/config.json +++ b/agentstack/tools/directory_search/config.json @@ -1,6 +1,9 @@ { "name": "directory_search", - "url": "https://github.com/crewAIInc/crewAI-tools/tree/main/crewai_tools/tools/directory_search_tool", "category": "computer-control", - "tools": ["dir_search_tool"] -} \ No newline at end of file + "tools": ["search_directory", "search_fixed_directory"], + "description": "Search through files in a directory using embedchain's DirectoryLoader", + "env": { + "DIRECTORY_SEARCH_PATH": "Optional. Path to directory for fixed directory search." + } +} diff --git a/agentstack/tools/directory_search/directory_search.py b/agentstack/tools/directory_search/directory_search.py new file mode 100644 index 00000000..76390335 --- /dev/null +++ b/agentstack/tools/directory_search/directory_search.py @@ -0,0 +1,39 @@ +from typing import Optional +from embedchain.loaders.directory_loader import DirectoryLoader +import os + + +def search_directory(directory: str, query: str) -> str: + """ + Search through files in a directory using embedchain's DirectoryLoader. + + Args: + directory: Path to directory to search + query: Search query to find relevant content + + Returns: + str: Search results as a string + """ + loader = DirectoryLoader(config=dict(recursive=True)) + results = loader.search(directory, query) + return str(results) + + +def search_fixed_directory(query: str) -> str: + """ + Search through files in a preconfigured directory using embedchain's DirectoryLoader. + Uses DIRECTORY_SEARCH_PATH environment variable. + + Args: + query: Search query to find relevant content + + Returns: + str: Search results as a string + + Raises: + ValueError: If DIRECTORY_SEARCH_PATH environment variable is not set + """ + directory = os.getenv('DIRECTORY_SEARCH_PATH') + if not directory: + raise ValueError("DIRECTORY_SEARCH_PATH environment variable not set") + return search_directory(directory, query) From c403690897fbf2e3a4cee941e72c7ab81ab10857 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:52:43 +0000 Subject: [PATCH 10/34] refactor: make directory_search tool framework-agnostic Co-Authored-By: root@a10k.co --- agentstack/tools/directory_search/__init__.py | 4 +++ agentstack/tools/directory_search/config.json | 9 +++-- .../directory_search/directory_search.py | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 agentstack/tools/directory_search/directory_search.py diff --git a/agentstack/tools/directory_search/__init__.py b/agentstack/tools/directory_search/__init__.py index e69de29b..2be73688 100644 --- a/agentstack/tools/directory_search/__init__.py +++ b/agentstack/tools/directory_search/__init__.py @@ -0,0 +1,4 @@ +"""Directory search tool for searching through files in a directory.""" +from .directory_search import search_directory, search_fixed_directory + +__all__ = ["search_directory", "search_fixed_directory"] diff --git a/agentstack/tools/directory_search/config.json b/agentstack/tools/directory_search/config.json index b412d03b..a8b83c8d 100644 --- a/agentstack/tools/directory_search/config.json +++ b/agentstack/tools/directory_search/config.json @@ -1,6 +1,9 @@ { "name": "directory_search", - "url": "https://github.com/crewAIInc/crewAI-tools/tree/main/crewai_tools/tools/directory_search_tool", "category": "computer-control", - "tools": ["dir_search_tool"] -} \ No newline at end of file + "tools": ["search_directory", "search_fixed_directory"], + "description": "Search through files in a directory using embedchain's DirectoryLoader", + "env": { + "DIRECTORY_SEARCH_PATH": "Optional. Path to directory for fixed directory search." + } +} diff --git a/agentstack/tools/directory_search/directory_search.py b/agentstack/tools/directory_search/directory_search.py new file mode 100644 index 00000000..ab846acb --- /dev/null +++ b/agentstack/tools/directory_search/directory_search.py @@ -0,0 +1,34 @@ +"""Framework-agnostic directory search implementation using embedchain.""" +import os +from embedchain import DirectoryLoader + +def search_directory(directory: str, query: str) -> str: + """Search through files in a specified directory using embedchain's DirectoryLoader. + + Args: + directory: Path to the directory to search + query: Search query string + + Returns: + str: Search results as a string + """ + loader = DirectoryLoader(directory) + results = loader.search(query) + return str(results) + +def search_fixed_directory(query: str) -> str: + """Search through files in a preconfigured directory using embedchain's DirectoryLoader. + + Args: + query: Search query string + + Returns: + str: Search results as a string + + Raises: + ValueError: If DIRECTORY_SEARCH_PATH environment variable is not set + """ + directory = os.getenv("DIRECTORY_SEARCH_PATH") + if not directory: + raise ValueError("DIRECTORY_SEARCH_PATH environment variable must be set") + return search_directory(directory, query) From 7c75b9c209cbdebb4611fa6b6a9d230f0338517c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 23:59:12 +0000 Subject: [PATCH 11/34] chore: add embedchain dependency to directory_search tool Co-Authored-By: root@a10k.co --- agentstack/tools/directory_search/config.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agentstack/tools/directory_search/config.json b/agentstack/tools/directory_search/config.json index a8b83c8d..d23d6618 100644 --- a/agentstack/tools/directory_search/config.json +++ b/agentstack/tools/directory_search/config.json @@ -5,5 +5,8 @@ "description": "Search through files in a directory using embedchain's DirectoryLoader", "env": { "DIRECTORY_SEARCH_PATH": "Optional. Path to directory for fixed directory search." - } + }, + "dependencies": [ + "embedchain>=0.1.0" + ] } From 2f6c78789e8d0046ce658276db4d25f234a7386c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:25:44 +0000 Subject: [PATCH 12/34] refactor: make composio tool framework-agnostic - Port implementation from crewAI-tools - Make functions framework-agnostic - Remove unnecessary framework-specific files - Update config.json with correct dependencies Co-Authored-By: root@a10k.co --- agentstack/tools/composio/__init__.py | 75 ++++++++++++++++++++++++ agentstack/tools/composio/config.json | 14 +++-- agentstack/tools/composio/crewai.py | 8 --- agentstack/tools/composio/pyproject.toml | 10 ---- 4 files changed, 85 insertions(+), 22 deletions(-) delete mode 100644 agentstack/tools/composio/crewai.py delete mode 100644 agentstack/tools/composio/pyproject.toml diff --git a/agentstack/tools/composio/__init__.py b/agentstack/tools/composio/__init__.py index e69de29b..8b62d046 100644 --- a/agentstack/tools/composio/__init__.py +++ b/agentstack/tools/composio/__init__.py @@ -0,0 +1,75 @@ +"""Framework-agnostic implementation of composio tools.""" + +import os +from typing import Any, Dict, List, Optional + +from composio import Action, ComposioToolSet +from composio.constants import DEFAULT_ENTITY_ID + + +def _check_connected_account(app: str, toolset: ComposioToolSet) -> None: + """Check if connected account exists for the app.""" + connections = toolset.client.connected_accounts.get() + if app not in [connection.appUniqueId for connection in connections]: + raise RuntimeError( + f"No connected account found for app `{app}`; " f"Run `composio add {app}` to fix this" + ) + + +def execute_action( + action_name: str, + params: Dict[str, Any], + entity_id: Optional[str] = None, + no_auth: bool = False, +) -> Dict[str, Any]: + """ + Execute a composio action with given parameters. + + Args: + action_name: Name of the action to execute + params: Parameters for the action + entity_id: Optional entity ID (defaults to DEFAULT_ENTITY_ID) + no_auth: Whether the action requires authentication + + Returns: + Dict containing the action result + """ + toolset = ComposioToolSet() + action = Action(action_name) + + if not no_auth: + _check_connected_account(action.app, toolset) + + return toolset.execute_action( + action=action, + params=params, + entity_id=entity_id or DEFAULT_ENTITY_ID, + ) + + +def get_action_schema(action_name: str) -> Dict[str, Any]: + """Get the schema for a composio action.""" + toolset = ComposioToolSet() + action = Action(action_name) + (action_schema,) = toolset.get_action_schemas(actions=[action]) + return action_schema.model_dump(exclude_none=True) + + +def find_actions_by_use_case( + *apps: str, + use_case: str, +) -> List[Dict[str, Any]]: + """Find actions by use case.""" + toolset = ComposioToolSet() + actions = toolset.find_actions_by_use_case(*apps, use_case=use_case) + return [get_action_schema(action.name) for action in actions] + + +def find_actions_by_tags( + *apps: str, + tags: List[str], +) -> List[Dict[str, Any]]: + """Find actions by tags.""" + toolset = ComposioToolSet() + actions = toolset.find_actions_by_tags(*apps, tags=tags) + return [get_action_schema(action.name) for action in actions] diff --git a/agentstack/tools/composio/config.json b/agentstack/tools/composio/config.json index 5a5dfb33..42a292c4 100644 --- a/agentstack/tools/composio/config.json +++ b/agentstack/tools/composio/config.json @@ -5,7 +5,13 @@ "env": { "COMPOSIO_API_KEY": "..." }, - "tools": ["composio_tools"], - "tools_bundled": true, - "cta": "!!! Composio provides 150+ tools. Additional setup is required in src/tools/composio_tool.py" -} \ No newline at end of file + "tools": [ + "execute_action", + "get_action_schema", + "find_actions_by_use_case", + "find_actions_by_tags" + ], + "dependencies": [ + "composio>=1.0.0" + ] +} diff --git a/agentstack/tools/composio/crewai.py b/agentstack/tools/composio/crewai.py deleted file mode 100644 index fde2fc88..00000000 --- a/agentstack/tools/composio/crewai.py +++ /dev/null @@ -1,8 +0,0 @@ -from composio_crewai import ComposioToolSet, App - -composio_tools = ComposioToolSet().get_tools(apps=[App.CODEINTERPRETER]) -# composio_tools = ComposioToolSet().get_tools(apps=[App.GITHUB]) -# etc - -# change App.CODEINTERPRETER to be the app you want to use -# For more info on tool selection, see https://docs.agentstack.sh/tools/tool/composio diff --git a/agentstack/tools/composio/pyproject.toml b/agentstack/tools/composio/pyproject.toml deleted file mode 100644 index 4afbc383..00000000 --- a/agentstack/tools/composio/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "agentstack.composio" -version = "0.1" - -dependencies = [] - -[project.optional-dependencies] -crewai = [ - "composio-crewai" -] From 1c626865ee60187468a819e886ac556cf64a024a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:29:32 +0000 Subject: [PATCH 13/34] chore: restore cta field in composio tool config - Add back cta field to indicate Composio's extensive tool offerings - Update setup instructions to reference __init__.py Co-Authored-By: root@a10k.co --- agentstack/tools/composio/config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agentstack/tools/composio/config.json b/agentstack/tools/composio/config.json index 42a292c4..cc9c2bd6 100644 --- a/agentstack/tools/composio/config.json +++ b/agentstack/tools/composio/config.json @@ -13,5 +13,6 @@ ], "dependencies": [ "composio>=1.0.0" - ] + ], + "cta": "!!! Composio provides 150+ tools. Additional setup is required in agentstack/tools/composio/__init__.py" } From 459474de0df9693a48882341d4f56ef99add8388 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:39:12 +0000 Subject: [PATCH 14/34] refactor: make vision tool framework-agnostic Co-Authored-By: root@a10k.co --- agentstack/tools/vision/__init__.py | 4 ++ agentstack/tools/vision/config.json | 13 +++-- agentstack/tools/vision/crewai.py | 3 -- agentstack/tools/vision/pyproject.toml | 8 --- agentstack/tools/vision/vision.py | 74 ++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 14 deletions(-) delete mode 100644 agentstack/tools/vision/crewai.py delete mode 100644 agentstack/tools/vision/pyproject.toml create mode 100644 agentstack/tools/vision/vision.py diff --git a/agentstack/tools/vision/__init__.py b/agentstack/tools/vision/__init__.py index e69de29b..55869473 100644 --- a/agentstack/tools/vision/__init__.py +++ b/agentstack/tools/vision/__init__.py @@ -0,0 +1,4 @@ +"""Vision tool for analyzing images using OpenAI's Vision API.""" +from .vision import analyze_image + +__all__ = ["analyze_image"] diff --git a/agentstack/tools/vision/config.json b/agentstack/tools/vision/config.json index e26e4e43..37963f0d 100644 --- a/agentstack/tools/vision/config.json +++ b/agentstack/tools/vision/config.json @@ -1,5 +1,12 @@ { "name": "vision", - "category": "vision", - "tools": ["vision_tool"] -} \ No newline at end of file + "category": "image-analysis", + "env": { + "OPENAI_API_KEY": null + }, + "dependencies": [ + "openai>=1.0.0", + "requests>=2.31.0" + ], + "tools": ["analyze_image"] +} diff --git a/agentstack/tools/vision/crewai.py b/agentstack/tools/vision/crewai.py deleted file mode 100644 index b2434b96..00000000 --- a/agentstack/tools/vision/crewai.py +++ /dev/null @@ -1,3 +0,0 @@ -from crewai_tools import VisionTool - -vision_tool = VisionTool() diff --git a/agentstack/tools/vision/pyproject.toml b/agentstack/tools/vision/pyproject.toml deleted file mode 100644 index cf61ae98..00000000 --- a/agentstack/tools/vision/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "agentstack.vision" -version = "0.1" - -dependencies = [] - -[project.optional-dependencies] -crewai = [] diff --git a/agentstack/tools/vision/vision.py b/agentstack/tools/vision/vision.py new file mode 100644 index 00000000..8d02c958 --- /dev/null +++ b/agentstack/tools/vision/vision.py @@ -0,0 +1,74 @@ +import base64 +from typing import Optional +import requests +from openai import OpenAI + + +def analyze_image(image_path_url: str) -> str: + """ + Analyze an image using OpenAI's Vision API. + + Args: + image_path_url: Local path or URL to the image + + Returns: + str: Description of the image contents + """ + client = OpenAI() + + if not image_path_url: + return "Image Path or URL is required." + + if "http" in image_path_url: + return _analyze_web_image(client, image_path_url) + return _analyze_local_image(client, image_path_url) + + +def _analyze_web_image(client: OpenAI, image_path_url: str) -> str: + response = client.chat.completions.create( + model="gpt-4-vision-preview", + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + {"type": "image_url", "image_url": {"url": image_path_url}} + ] + }], + max_tokens=300 + ) + return response.choices[0].message.content + + +def _analyze_local_image(client: OpenAI, image_path: str) -> str: + base64_image = _encode_image(image_path) + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {client.api_key}" + } + payload = { + "model": "gpt-4-vision-preview", + "messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + } + } + ] + }], + "max_tokens": 300 + } + response = requests.post( + "https://api.openai.com/v1/chat/completions", + headers=headers, + json=payload + ) + return response.json()["choices"][0]["message"]["content"] + + +def _encode_image(image_path: str) -> str: + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") From d60da5b0d4ed484364dc30733806211449e43ab3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:41:31 +0000 Subject: [PATCH 15/34] refactor: namespace agent_connect environment variables with AGENT_CONNECT_ prefix Co-Authored-By: root@a10k.co --- agentstack/tools/agent_connect/__init__.py | 16 ++++++++-------- agentstack/tools/agent_connect/config.json | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py index ff40bab5..66b1d525 100644 --- a/agentstack/tools/agent_connect/__init__.py +++ b/agentstack/tools/agent_connect/__init__.py @@ -5,27 +5,27 @@ # An HTTP and WS service will be started in agent-connect # It can be an IP address or a domain name -host_domain = os.getenv("HOST_DOMAIN") +host_domain = os.getenv("AGENT_CONNECT_HOST_DOMAIN") # Host port, default is 80 -host_port = os.getenv("HOST_PORT") +host_port = os.getenv("AGENT_CONNECT_HOST_PORT") # WS path, default is /ws -host_ws_path = os.getenv("HOST_WS_PATH") +host_ws_path = os.getenv("AGENT_CONNECT_HOST_WS_PATH") # Path to store DID document -did_document_path = os.getenv("DID_DOCUMENT_PATH") +did_document_path = os.getenv("AGENT_CONNECT_DID_DOCUMENT_PATH") # SSL certificate path, if using HTTPS, certificate and key need to be provided -ssl_cert_path = os.getenv("SSL_CERT_PATH") -ssl_key_path = os.getenv("SSL_KEY_PATH") +ssl_cert_path = os.getenv("AGENT_CONNECT_SSL_CERT_PATH") +ssl_key_path = os.getenv("AGENT_CONNECT_SSL_KEY_PATH") if not host_domain: raise Exception(( "Host domain has not been provided.\n" - "Did you set the HOST_DOMAIN in you project's .env file?" + "Did you set the AGENT_CONNECT_HOST_DOMAIN in you project's .env file?" )) if not did_document_path: raise Exception(( "DID document path has not been provided.\n" - "Did you set the DID_DOCUMENT_PATH in you project's .env file?" + "Did you set the AGENT_CONNECT_DID_DOCUMENT_PATH in you project's .env file?" )) diff --git a/agentstack/tools/agent_connect/config.json b/agentstack/tools/agent_connect/config.json index 97e2c589..542c81e8 100644 --- a/agentstack/tools/agent_connect/config.json +++ b/agentstack/tools/agent_connect/config.json @@ -3,12 +3,12 @@ "url": "https://github.com/chgaowei/AgentConnect", "category": "network-protocols", "env": { - "HOST_DOMAIN": null, - "HOST_PORT": 80, - "HOST_WS_PATH": "/ws", - "DID_DOCUMENT_PATH": "data/agent_connect_did.json", - "SSL_CERT_PATH": null, - "SSL_KEY_PATH": null + "AGENT_CONNECT_HOST_DOMAIN": null, + "AGENT_CONNECT_HOST_PORT": 80, + "AGENT_CONNECT_HOST_WS_PATH": "/ws", + "AGENT_CONNECT_DID_DOCUMENT_PATH": "data/agent_connect_did.json", + "AGENT_CONNECT_SSL_CERT_PATH": null, + "AGENT_CONNECT_SSL_KEY_PATH": null }, "dependencies": [ "agent-connect>=0.3.0" From 8a24f14169beab82eae8d1ddc911d7843b3ac167 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 01:04:37 +0000 Subject: [PATCH 16/34] refactor: namespace agent_connect environment variables with AGENT_CONNECT_ prefix Co-Authored-By: root@a10k.co --- agentstack/tools/agent_connect/__init__.py | 34 ++++++++++++---------- agentstack/tools/agent_connect/config.json | 12 ++++---- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py index ff40bab5..2bf3115d 100644 --- a/agentstack/tools/agent_connect/__init__.py +++ b/agentstack/tools/agent_connect/__init__.py @@ -5,28 +5,32 @@ # An HTTP and WS service will be started in agent-connect # It can be an IP address or a domain name -host_domain = os.getenv("HOST_DOMAIN") +host_domain = os.getenv("AGENT_CONNECT_HOST_DOMAIN") # Host port, default is 80 -host_port = os.getenv("HOST_PORT") +host_port = os.getenv("AGENT_CONNECT_HOST_PORT") # WS path, default is /ws -host_ws_path = os.getenv("HOST_WS_PATH") +host_ws_path = os.getenv("AGENT_CONNECT_HOST_WS_PATH") # Path to store DID document -did_document_path = os.getenv("DID_DOCUMENT_PATH") +did_document_path = os.getenv("AGENT_CONNECT_DID_DOCUMENT_PATH") # SSL certificate path, if using HTTPS, certificate and key need to be provided -ssl_cert_path = os.getenv("SSL_CERT_PATH") -ssl_key_path = os.getenv("SSL_KEY_PATH") +ssl_cert_path = os.getenv("AGENT_CONNECT_SSL_CERT_PATH") +ssl_key_path = os.getenv("AGENT_CONNECT_SSL_KEY_PATH") if not host_domain: - raise Exception(( - "Host domain has not been provided.\n" - "Did you set the HOST_DOMAIN in you project's .env file?" - )) + raise Exception( + ( + "Host domain has not been provided.\n" + "Did you set the AGENT_CONNECT_HOST_DOMAIN in you project's .env file?" + ) + ) if not did_document_path: - raise Exception(( - "DID document path has not been provided.\n" - "Did you set the DID_DOCUMENT_PATH in you project's .env file?" - )) + raise Exception( + ( + "DID document path has not been provided.\n" + "Did you set the AGENT_CONNECT_DID_DOCUMENT_PATH in you project's .env file?" + ) + ) def generate_did_info(node: SimpleNode, did_document_path: str) -> None: @@ -48,7 +52,7 @@ def generate_did_info(node: SimpleNode, did_document_path: str) -> None: node.set_did_info(private_key_pem, did, did_document_json) # Save DID information - if os.path.dirname(did_document_path): # allow saving to current directory + if os.path.dirname(did_document_path): # allow saving to current directory os.makedirs(os.path.dirname(did_document_path), exist_ok=True) with open(did_document_path, "w") as f: json.dump( diff --git a/agentstack/tools/agent_connect/config.json b/agentstack/tools/agent_connect/config.json index 97e2c589..542c81e8 100644 --- a/agentstack/tools/agent_connect/config.json +++ b/agentstack/tools/agent_connect/config.json @@ -3,12 +3,12 @@ "url": "https://github.com/chgaowei/AgentConnect", "category": "network-protocols", "env": { - "HOST_DOMAIN": null, - "HOST_PORT": 80, - "HOST_WS_PATH": "/ws", - "DID_DOCUMENT_PATH": "data/agent_connect_did.json", - "SSL_CERT_PATH": null, - "SSL_KEY_PATH": null + "AGENT_CONNECT_HOST_DOMAIN": null, + "AGENT_CONNECT_HOST_PORT": 80, + "AGENT_CONNECT_HOST_WS_PATH": "/ws", + "AGENT_CONNECT_DID_DOCUMENT_PATH": "data/agent_connect_did.json", + "AGENT_CONNECT_SSL_CERT_PATH": null, + "AGENT_CONNECT_SSL_KEY_PATH": null }, "dependencies": [ "agent-connect>=0.3.0" From e99eaa1a146e456d68ff08cf3e5d31383cd197ca Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Wed, 18 Dec 2024 18:37:49 -0800 Subject: [PATCH 17/34] Prefix environment variables in agent_connect tool --- agentstack/tools/agent_connect/__init__.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py index 2bf3115d..44528a39 100644 --- a/agentstack/tools/agent_connect/__init__.py +++ b/agentstack/tools/agent_connect/__init__.py @@ -17,21 +17,16 @@ ssl_key_path = os.getenv("AGENT_CONNECT_SSL_KEY_PATH") if not host_domain: - raise Exception( - ( - "Host domain has not been provided.\n" - "Did you set the AGENT_CONNECT_HOST_DOMAIN in you project's .env file?" - ) - ) + raise Exception(( + "Host domain has not been provided.\n" + "Did you set the AGENT_CONNECT_HOST_DOMAIN in you project's .env file?" + )) if not did_document_path: - raise Exception( - ( - "DID document path has not been provided.\n" - "Did you set the AGENT_CONNECT_DID_DOCUMENT_PATH in you project's .env file?" - ) - ) - + raise Exception(( + "DID document path has not been provided.\n" + "Did you set the AGENT_CONNECT_DID_DOCUMENT_PATH in you project's .env file?" + )) def generate_did_info(node: SimpleNode, did_document_path: str) -> None: """ From 9933cad6968217e195857161bd9cde57b382da35 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:28:01 +0000 Subject: [PATCH 18/34] refactor: make file_read tool framework-agnostic Co-Authored-By: root@a10k.co --- agentstack/tools/file_read/__init__.py | 32 ++++++++++++++++++++++++++ agentstack/tools/file_read/config.json | 8 ++++--- agentstack/tools/file_read/crewai.py | 3 --- 3 files changed, 37 insertions(+), 6 deletions(-) delete mode 100644 agentstack/tools/file_read/crewai.py diff --git a/agentstack/tools/file_read/__init__.py b/agentstack/tools/file_read/__init__.py index e69de29b..1ab49990 100644 --- a/agentstack/tools/file_read/__init__.py +++ b/agentstack/tools/file_read/__init__.py @@ -0,0 +1,32 @@ +""" +Framework-agnostic implementation of file reading functionality. +""" +from typing import Optional +from pathlib import Path + + +def read_file(file_path: str) -> str: + """Read contents of a file at the given path. + + Args: + file_path: Path to the file to read + + Returns: + str: The contents of the file as a string + + Raises: + FileNotFoundError: If the file does not exist + PermissionError: If the file cannot be accessed + Exception: For other file reading errors + """ + try: + path = Path(file_path).resolve() + if not path.exists(): + return f"Error: File not found at path {file_path}" + if not path.is_file(): + return f"Error: Path {file_path} is not a file" + + with open(path, "r", encoding="utf-8") as file: + return file.read() + except (FileNotFoundError, PermissionError, Exception) as e: + return f"Failed to read file {file_path}. Error: {str(e)}" diff --git a/agentstack/tools/file_read/config.json b/agentstack/tools/file_read/config.json index f1b072eb..1d3118ac 100644 --- a/agentstack/tools/file_read/config.json +++ b/agentstack/tools/file_read/config.json @@ -1,6 +1,8 @@ { "name": "file_read", - "url": "https://github.com/crewAIInc/crewAI-tools/tree/main/crewai_tools/tools/file_read_tool", "category": "computer-control", - "tools": ["file_read_tool"] -} \ No newline at end of file + "tools": ["read_file"], + "description": "Read contents of files", + "url": "https://github.com/AgentOps-AI/AgentStack/tree/main/agentstack/tools/file_read", + "dependencies": [] +} diff --git a/agentstack/tools/file_read/crewai.py b/agentstack/tools/file_read/crewai.py deleted file mode 100644 index 5fa6cadd..00000000 --- a/agentstack/tools/file_read/crewai.py +++ /dev/null @@ -1,3 +0,0 @@ -from crewai_tools import FileReadTool - -file_read_tool = FileReadTool() From 76be2155724abc9863bc7a553b9d3dcaba1949d6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 22:37:38 +0000 Subject: [PATCH 19/34] refactor: move directory_search implementation to __init__.py Co-Authored-By: root@a10k.co --- agentstack/tools/directory_search/__init__.py | 43 +++++++++++++++++-- agentstack/tools/directory_search/config.json | 2 +- agentstack/tools/directory_search/crewai.py | 25 ----------- .../directory_search/directory_search.py | 40 ----------------- 4 files changed, 41 insertions(+), 69 deletions(-) delete mode 100644 agentstack/tools/directory_search/crewai.py delete mode 100644 agentstack/tools/directory_search/directory_search.py diff --git a/agentstack/tools/directory_search/__init__.py b/agentstack/tools/directory_search/__init__.py index dd096ced..04a53ab6 100644 --- a/agentstack/tools/directory_search/__init__.py +++ b/agentstack/tools/directory_search/__init__.py @@ -1,4 +1,41 @@ -"""Directory search tool for searching through files in a directory.""" -from .directory_search import search_directory, search_fixed_directory +"""Framework-agnostic directory search implementation using embedchain.""" +from typing import Optional +from pathlib import Path +from embedchain.loaders.directory_loader import DirectoryLoader +import os -__all__ = ['search_directory', 'search_fixed_directory'] + +def search_directory(directory: str, query: str) -> str: + """ + Search through files in a directory using embedchain's DirectoryLoader. + + Args: + directory: Path to directory to search + query: Search query to find relevant content + + Returns: + str: Search results as a string + """ + loader = DirectoryLoader(config=dict(recursive=True)) + results = loader.search(directory, query) + return str(results) + + +def search_fixed_directory(query: str) -> str: + """ + Search through files in a preconfigured directory using embedchain's DirectoryLoader. + Uses DIRECTORY_SEARCH_TOOL_PATH environment variable. + + Args: + query: Search query to find relevant content + + Returns: + str: Search results as a string + + Raises: + ValueError: If DIRECTORY_SEARCH_TOOL_PATH environment variable is not set + """ + directory = os.getenv('DIRECTORY_SEARCH_TOOL_PATH') + if not directory: + raise ValueError("DIRECTORY_SEARCH_TOOL_PATH environment variable not set") + return search_directory(directory, query) diff --git a/agentstack/tools/directory_search/config.json b/agentstack/tools/directory_search/config.json index d23d6618..be5d8e2b 100644 --- a/agentstack/tools/directory_search/config.json +++ b/agentstack/tools/directory_search/config.json @@ -4,7 +4,7 @@ "tools": ["search_directory", "search_fixed_directory"], "description": "Search through files in a directory using embedchain's DirectoryLoader", "env": { - "DIRECTORY_SEARCH_PATH": "Optional. Path to directory for fixed directory search." + "DIRECTORY_SEARCH_TOOL_PATH": "Optional. Path to directory for fixed directory search." }, "dependencies": [ "embedchain>=0.1.0" diff --git a/agentstack/tools/directory_search/crewai.py b/agentstack/tools/directory_search/crewai.py deleted file mode 100644 index d0d36710..00000000 --- a/agentstack/tools/directory_search/crewai.py +++ /dev/null @@ -1,25 +0,0 @@ -from crewai_tools import DirectorySearchTool - -# dir_search_tool = DirectorySearchTool( -# config=dict( -# llm=dict( -# provider="ollama", -# config=dict( -# model="llama2", -# # temperature=0.5, -# # top_p=1, -# # stream=true, -# ), -# ), -# embedder=dict( -# provider="google", -# config=dict( -# model="models/embedding-001", -# task_type="retrieval_document", -# # title="Embeddings", -# ), -# ), -# ) -# ) - -dir_search_tool = DirectorySearchTool() diff --git a/agentstack/tools/directory_search/directory_search.py b/agentstack/tools/directory_search/directory_search.py deleted file mode 100644 index b0e00ee9..00000000 --- a/agentstack/tools/directory_search/directory_search.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Framework-agnostic directory search implementation using embedchain.""" -from typing import Optional -from embedchain.loaders.directory_loader import DirectoryLoader -import os - - -def search_directory(directory: str, query: str) -> str: - """ - Search through files in a directory using embedchain's DirectoryLoader. - - Args: - directory: Path to directory to search - query: Search query to find relevant content - - Returns: - str: Search results as a string - """ - loader = DirectoryLoader(config=dict(recursive=True)) - results = loader.search(directory, query) - return str(results) - - -def search_fixed_directory(query: str) -> str: - """ - Search through files in a preconfigured directory using embedchain's DirectoryLoader. - Uses DIRECTORY_SEARCH_PATH environment variable. - - Args: - query: Search query to find relevant content - - Returns: - str: Search results as a string - - Raises: - ValueError: If DIRECTORY_SEARCH_PATH environment variable is not set - """ - directory = os.getenv('DIRECTORY_SEARCH_PATH') - if not directory: - raise ValueError("DIRECTORY_SEARCH_PATH environment variable not set") - return search_directory(directory, query) From 4f4cf5ea5dbb8dea811c9f288fa506aff4feb130 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:01:45 +0000 Subject: [PATCH 20/34] refactor: make stripe tool framework-agnostic Co-Authored-By: root@a10k.co --- agentstack/tools/stripe/__init__.py | 111 +++++++++++++++++++++++++ agentstack/tools/stripe/config.json | 22 ++--- agentstack/tools/stripe/crewai.py | 14 ---- agentstack/tools/stripe/pyproject.toml | 11 --- 4 files changed, 123 insertions(+), 35 deletions(-) delete mode 100644 agentstack/tools/stripe/crewai.py delete mode 100644 agentstack/tools/stripe/pyproject.toml diff --git a/agentstack/tools/stripe/__init__.py b/agentstack/tools/stripe/__init__.py index e69de29b..fc750b43 100644 --- a/agentstack/tools/stripe/__init__.py +++ b/agentstack/tools/stripe/__init__.py @@ -0,0 +1,111 @@ +"""Stripe tool for AgentStack. + +This module provides framework-agnostic functions for interacting with the Stripe API, +supporting payment links, products, and prices operations. +""" + +import json +import os +from typing import Dict, Optional + +import stripe + +def setup_stripe(secret_key: str) -> None: + """Initialize Stripe with the provided secret key.""" + stripe.api_key = secret_key + stripe.set_app_info( + "agentstack-stripe", + version="0.1.0", + url="https://github.com/AgentOps-AI/AgentStack" + ) + +def create_payment_link(price: str, quantity: int) -> Dict: + """Create a payment link for a specific price and quantity. + + Args: + price: The ID of the price to create a payment link for + quantity: The quantity of items to include in the payment link + + Returns: + Dict containing the payment link ID and URL + """ + payment_link = stripe.PaymentLink.create( + line_items=[{"price": price, "quantity": quantity}] + ) + return {"id": payment_link.id, "url": payment_link.url} + +def get_payment_link(payment_link_id: str) -> Dict: + """Retrieve a payment link by ID. + + Args: + payment_link_id: The ID of the payment link to retrieve + + Returns: + Dict containing the payment link ID and URL + """ + payment_link = stripe.PaymentLink.retrieve(payment_link_id) + return {"id": payment_link.id, "url": payment_link.url} + +def create_product(name: str, description: Optional[str] = None) -> Dict: + """Create a new product. + + Args: + name: The name of the product + description: Optional description of the product + + Returns: + Dict containing the product details + """ + product_data = {"name": name} + if description: + product_data["description"] = description + product = stripe.Product.create(**product_data) + return json.loads(str(product)) + +def update_product(product_id: str, **kwargs) -> Dict: + """Update an existing product. + + Args: + product_id: The ID of the product to update + **kwargs: Additional fields to update (e.g., name, description) + + Returns: + Dict containing the updated product details + """ + product = stripe.Product.modify(product_id, **kwargs) + return json.loads(str(product)) + +def create_price(product: str, currency: str, unit_amount: int) -> Dict: + """Create a new price for a product. + + Args: + product: The ID of the product to create a price for + currency: Three-letter ISO currency code + unit_amount: The amount in cents to charge + + Returns: + Dict containing the price details + """ + price = stripe.Price.create( + product=product, + currency=currency, + unit_amount=unit_amount + ) + return json.loads(str(price)) + +def update_price(price_id: str, **kwargs) -> Dict: + """Update an existing price. + + Args: + price_id: The ID of the price to update + **kwargs: Additional fields to update (e.g., nickname, active) + + Returns: + Dict containing the updated price details + """ + price = stripe.Price.modify(price_id, **kwargs) + return json.loads(str(price)) + +# Initialize Stripe when the module is imported if the environment variable is set +if "STRIPE_SECRET_KEY" in os.environ: + setup_stripe(os.environ["STRIPE_SECRET_KEY"]) diff --git a/agentstack/tools/stripe/config.json b/agentstack/tools/stripe/config.json index 9daa7d09..ea2ca899 100644 --- a/agentstack/tools/stripe/config.json +++ b/agentstack/tools/stripe/config.json @@ -1,11 +1,13 @@ { - "name": "stripe", - "url": "https://github.com/stripe/agent-toolkit", - "category": "application-specific", - "env": { - "STRIPE_SECRET_KEY": "sk-..." - }, - "tools_bundled": true, - "tools": ["stripe_tools"], - "cta": "🔑 Create your Stripe API key here: https://dashboard.stripe.com/account/apikeys" -} \ No newline at end of file + "name": "stripe", + "description": "Interact with Stripe API for payment processing", + "version": "0.1.0", + "dependencies": ["stripe>=11.0.0"], + "cta": "Visit https://stripe.com/docs/api for more information", + "environment_variables": { + "STRIPE_SECRET_KEY": { + "description": "Your Stripe API secret key", + "required": true + } + } +} diff --git a/agentstack/tools/stripe/crewai.py b/agentstack/tools/stripe/crewai.py deleted file mode 100644 index 8da76619..00000000 --- a/agentstack/tools/stripe/crewai.py +++ /dev/null @@ -1,14 +0,0 @@ -import os -from stripe_agent_toolkit.crewai.toolkit import StripeAgentToolkit - - -stripe_tools = StripeAgentToolkit( - secret_key=os.getenv("STRIPE_SECRET_KEY"), - configuration={ - "actions": { - "payment_links": {"create": True, "read": True, "update": False}, - "products": {"create": True, "update": True}, - "prices": {"create": True, "update": True}, - } - }, -).get_tools() diff --git a/agentstack/tools/stripe/pyproject.toml b/agentstack/tools/stripe/pyproject.toml deleted file mode 100644 index bc427455..00000000 --- a/agentstack/tools/stripe/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "agentstack.stripe" -version = "0.1" - -dependencies = [ - "stripe-agent-toolkit", - "stripe", -] - -[project.optional-dependencies] -crewai = [] From 5da42483e91e9c015635161953aa91c41498f5cb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:09:13 +0000 Subject: [PATCH 21/34] refactor: use stripe_agent_toolkit functions for stripe tool Co-Authored-By: root@a10k.co --- agentstack/tools/stripe/__init__.py | 45 ++++++++++++++++------------- agentstack/tools/stripe/config.json | 5 +++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/agentstack/tools/stripe/__init__.py b/agentstack/tools/stripe/__init__.py index fc750b43..ee4bd14d 100644 --- a/agentstack/tools/stripe/__init__.py +++ b/agentstack/tools/stripe/__init__.py @@ -9,15 +9,20 @@ from typing import Dict, Optional import stripe +from stripe_agent_toolkit.functions import ( + create_payment_link as toolkit_create_payment_link, + create_product as toolkit_create_product, + create_price as toolkit_create_price, +) +from stripe_agent_toolkit.configuration import Context +from stripe_agent_toolkit.schema import CreatePaymentLink, CreateProduct, CreatePrice + def setup_stripe(secret_key: str) -> None: """Initialize Stripe with the provided secret key.""" stripe.api_key = secret_key - stripe.set_app_info( - "agentstack-stripe", - version="0.1.0", - url="https://github.com/AgentOps-AI/AgentStack" - ) + stripe.set_app_info("agentstack-stripe", version="0.1.0", url="https://github.com/AgentOps-AI/AgentStack") + def create_payment_link(price: str, quantity: int) -> Dict: """Create a payment link for a specific price and quantity. @@ -29,10 +34,10 @@ def create_payment_link(price: str, quantity: int) -> Dict: Returns: Dict containing the payment link ID and URL """ - payment_link = stripe.PaymentLink.create( - line_items=[{"price": price, "quantity": quantity}] - ) - return {"id": payment_link.id, "url": payment_link.url} + # Validate input using toolkit's schema + params = CreatePaymentLink(price=price, quantity=quantity) + return toolkit_create_payment_link(Context(), params.price, params.quantity) + def get_payment_link(payment_link_id: str) -> Dict: """Retrieve a payment link by ID. @@ -46,6 +51,7 @@ def get_payment_link(payment_link_id: str) -> Dict: payment_link = stripe.PaymentLink.retrieve(payment_link_id) return {"id": payment_link.id, "url": payment_link.url} + def create_product(name: str, description: Optional[str] = None) -> Dict: """Create a new product. @@ -56,11 +62,10 @@ def create_product(name: str, description: Optional[str] = None) -> Dict: Returns: Dict containing the product details """ - product_data = {"name": name} - if description: - product_data["description"] = description - product = stripe.Product.create(**product_data) - return json.loads(str(product)) + # Validate input using toolkit's schema + params = CreateProduct(name=name, description=description) + return toolkit_create_product(Context(), params.name, params.description) + def update_product(product_id: str, **kwargs) -> Dict: """Update an existing product. @@ -75,6 +80,7 @@ def update_product(product_id: str, **kwargs) -> Dict: product = stripe.Product.modify(product_id, **kwargs) return json.loads(str(product)) + def create_price(product: str, currency: str, unit_amount: int) -> Dict: """Create a new price for a product. @@ -86,12 +92,10 @@ def create_price(product: str, currency: str, unit_amount: int) -> Dict: Returns: Dict containing the price details """ - price = stripe.Price.create( - product=product, - currency=currency, - unit_amount=unit_amount - ) - return json.loads(str(price)) + # Validate input using toolkit's schema + params = CreatePrice(product=product, currency=currency, unit_amount=unit_amount) + return toolkit_create_price(Context(), params.product, params.currency, params.unit_amount) + def update_price(price_id: str, **kwargs) -> Dict: """Update an existing price. @@ -106,6 +110,7 @@ def update_price(price_id: str, **kwargs) -> Dict: price = stripe.Price.modify(price_id, **kwargs) return json.loads(str(price)) + # Initialize Stripe when the module is imported if the environment variable is set if "STRIPE_SECRET_KEY" in os.environ: setup_stripe(os.environ["STRIPE_SECRET_KEY"]) diff --git a/agentstack/tools/stripe/config.json b/agentstack/tools/stripe/config.json index ea2ca899..c4d3dbd1 100644 --- a/agentstack/tools/stripe/config.json +++ b/agentstack/tools/stripe/config.json @@ -2,7 +2,10 @@ "name": "stripe", "description": "Interact with Stripe API for payment processing", "version": "0.1.0", - "dependencies": ["stripe>=11.0.0"], + "dependencies": [ + "stripe>=11.0.0", + "stripe-agent-toolkit>=0.2.0" + ], "cta": "Visit https://stripe.com/docs/api for more information", "environment_variables": { "STRIPE_SECRET_KEY": { From e3911dd6ce71ac9decd91fdc9749fc2ab1244dbe Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 19 Dec 2024 18:30:55 -0800 Subject: [PATCH 22/34] Fix stripe config.json --- agentstack/tools/stripe/config.json | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/agentstack/tools/stripe/config.json b/agentstack/tools/stripe/config.json index c4d3dbd1..2cac363e 100644 --- a/agentstack/tools/stripe/config.json +++ b/agentstack/tools/stripe/config.json @@ -1,16 +1,20 @@ { - "name": "stripe", - "description": "Interact with Stripe API for payment processing", - "version": "0.1.0", - "dependencies": [ - "stripe>=11.0.0", - "stripe-agent-toolkit>=0.2.0" - ], - "cta": "Visit https://stripe.com/docs/api for more information", - "environment_variables": { - "STRIPE_SECRET_KEY": { - "description": "Your Stripe API secret key", - "required": true - } - } -} + "name": "stripe", + "url": "https://github.com/stripe/agent-toolkit", + "category": "application-specific", + "env": { + "STRIPE_SECRET_KEY": null + }, + "dependencies": [ + "stripe-agent-toolkit>=0.2.0" + ], + "tools": [ + "create_payment_link", + "get_payment_link", + "create_product", + "update_product", + "create_price", + "update_price" + ], + "cta": "🔑 Create your Stripe API key here: https://dashboard.stripe.com/account/apikeys" +} \ No newline at end of file From b01d49f2ea17c0c216d9441a743efa4be9008021 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 19 Dec 2024 18:41:08 -0800 Subject: [PATCH 23/34] Delete agentstack/tools/file_read/pyproject.toml --- agentstack/tools/file_read/pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 agentstack/tools/file_read/pyproject.toml diff --git a/agentstack/tools/file_read/pyproject.toml b/agentstack/tools/file_read/pyproject.toml deleted file mode 100644 index a4f20180..00000000 --- a/agentstack/tools/file_read/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "agentstack.file_read" -version = "0.1" - -dependencies = [] - -[project.optional-dependencies] -crewai = [] From 2195c9e5064b318dd2afdc77254272ae85cb4067 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 20 Dec 2024 10:47:01 -0800 Subject: [PATCH 24/34] Reimplement stripe tool. Add __init__.py files to linter. --- .vscode/settings.json | 7 + agentstack/__init__.py | 24 ++- agentstack/cli/__init__.py | 2 +- agentstack/frameworks/__init__.py | 19 +- agentstack/frameworks/crewai.py | 6 +- agentstack/generation/__init__.py | 2 +- agentstack/generation/files.py | 4 +- agentstack/tools/__init__.py | 22 +- agentstack/tools/agent_connect/__init__.py | 9 +- agentstack/tools/browserbase/__init__.py | 2 +- agentstack/tools/code_interpreter/__init__.py | 22 +- agentstack/tools/directory_search/__init__.py | 1 + agentstack/tools/file_read/__init__.py | 1 + agentstack/tools/ftp/__init__.py | 23 +-- agentstack/tools/stripe/__init__.py | 189 +++++++----------- agentstack/tools/stripe/config.json | 8 +- agentstack/tools/vision/__init__.py | 1 + agentstack/tools/vision/vision.py | 52 ++--- pyproject.toml | 3 +- 19 files changed, 187 insertions(+), 210 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..4d4c9664 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "agentstack", + "asttools", + "crewai" + ] +} \ No newline at end of file diff --git a/agentstack/__init__.py b/agentstack/__init__.py index ca4e0a68..6fb354f7 100644 --- a/agentstack/__init__.py +++ b/agentstack/__init__.py @@ -1,9 +1,10 @@ """ -This it the beginning of the agentstack public API. +This it the beginning of the agentstack public API. Methods that have been imported into this file are expected to be used by the -end user inside of their project. +end user inside of their project. """ + from typing import Callable from pathlib import Path from agentstack import conf @@ -12,11 +13,11 @@ from agentstack import frameworks ___all___ = [ - "conf", - "tools", - "get_tags", - "get_framework", - "get_inputs", + "conf", + "tools", + "get_tags", + "get_framework", + "get_inputs", ] @@ -29,14 +30,15 @@ def get_tags() -> list[str]: class ToolLoader: """ - Provides the public interface for accessing tools, wrapped in the - framework-specific callable format. - + Provides the public interface for accessing tools, wrapped in the + framework-specific callable format. + Get a tool's callables by name with `agentstack.tools[tool_name]` Include them in your agent's tool list with `tools = [*agentstack.tools[tool_name], ]` """ + def __getitem__(self, tool_name: str) -> list[Callable]: return frameworks.get_tool_callables(tool_name) -tools = ToolLoader() +tools = ToolLoader() diff --git a/agentstack/cli/__init__.py b/agentstack/cli/__init__.py index 32c08ec3..49a5975f 100644 --- a/agentstack/cli/__init__.py +++ b/agentstack/cli/__init__.py @@ -1,3 +1,3 @@ from .cli import init_project_builder, configure_default_model, export_template from .tools import list_tools, add_tool -from .run import run_project \ No newline at end of file +from .run import run_project diff --git a/agentstack/frameworks/__init__.py b/agentstack/frameworks/__init__.py index 3659b621..ee33691b 100644 --- a/agentstack/frameworks/__init__.py +++ b/agentstack/frameworks/__init__.py @@ -11,12 +11,16 @@ CREWAI = 'crewai' -SUPPORTED_FRAMEWORKS = [CREWAI, ] +SUPPORTED_FRAMEWORKS = [ + CREWAI, +] + class FrameworkModule(Protocol): """ Protocol spec for a framework implementation module. """ + ENTRYPOINT: Path """ Relative path to the entrypoint file for the framework in the user's project. @@ -94,65 +98,74 @@ def get_framework_module(framework: str) -> FrameworkModule: except ImportError: raise Exception(f"Framework {framework} could not be imported.") + def get_entrypoint_path(framework: str) -> Path: """ Get the path to the entrypoint file for a framework. """ return conf.PATH / get_framework_module(framework).ENTRYPOINT + def validate_project(): """ Validate that the user's project is ready to run. """ return get_framework_module(get_framework()).validate_project() + def add_tool(tool: ToolConfig, agent_name: str): """ - Add a tool to the user's project. + Add a tool to the user's project. The tool will have aready been installed in the user's application and have all dependencies installed. We're just handling code generation here. """ return get_framework_module(get_framework()).add_tool(tool, agent_name) + def remove_tool(tool: ToolConfig, agent_name: str): """ Remove a tool from the user's project. """ return get_framework_module(get_framework()).remove_tool(tool, agent_name) + def get_tool_callables(tool_name: str) -> list[Callable]: """ Get a tool by name and return it as a list of framework-native callables. """ return get_framework_module(get_framework()).get_tool_callables(tool_name) + def get_agent_names() -> list[str]: """ Get a list of agent names in the user's project. """ return get_framework_module(get_framework()).get_agent_names() + def get_agent_tool_names(agent_name: str) -> list[str]: """ Get a list of tool names in the user's project. """ return get_framework_module(get_framework()).get_agent_tool_names(agent_name) + def add_agent(agent: AgentConfig): """ Add an agent to the user's project. """ return get_framework_module(get_framework()).add_agent(agent) + def add_task(task: TaskConfig): """ Add a task to the user's project. """ return get_framework_module(get_framework()).add_task(task) + def get_task_names() -> list[str]: """ Get a list of task names in the user's project. """ return get_framework_module(get_framework()).get_task_names() - diff --git a/agentstack/frameworks/crewai.py b/agentstack/frameworks/crewai.py index 8ace0123..973fbfff 100644 --- a/agentstack/frameworks/crewai.py +++ b/agentstack/frameworks/crewai.py @@ -117,7 +117,7 @@ def get_agent_tools(self, agent_name: str) -> ast.List: """ Get the tools used by an agent as AST nodes. - Tool definitons are inside of the methods marked with an `@agent` decorator. + Tool definitions are inside of the methods marked with an `@agent` decorator. The method returns a new class instance with the tools as a list of callables under the kwarg `tools`. """ @@ -186,7 +186,7 @@ def add_agent_tools(self, agent_name: str, tool: ToolConfig): """ Add new tools to be used by an agent. - Tool definitons are inside of the methods marked with an `@agent` decorator. + Tool definitions are inside of the methods marked with an `@agent` decorator. The method returns a new class instance with the tools as a list of callables under the kwarg `tools`. """ @@ -235,7 +235,7 @@ def remove_agent_tools(self, agent_name: str, tool: ToolConfig): def validate_project() -> None: """ Validate that a CrewAI project is ready to run. - Raises an `agentstack.VaidationError` if the project is not valid. + Raises an `agentstack.ValidationError` if the project is not valid. """ try: crew_file = CrewFile(conf.PATH / ENTRYPOINT) diff --git a/agentstack/generation/__init__.py b/agentstack/generation/__init__.py index 477b8999..ad0b3c90 100644 --- a/agentstack/generation/__init__.py +++ b/agentstack/generation/__init__.py @@ -1,4 +1,4 @@ from .agent_generation import add_agent from .task_generation import add_task from .tool_generation import add_tool, remove_tool -from .files import EnvFile, ProjectFile \ No newline at end of file +from .files import EnvFile, ProjectFile diff --git a/agentstack/generation/files.py b/agentstack/generation/files.py index fa218570..317925b4 100644 --- a/agentstack/generation/files.py +++ b/agentstack/generation/files.py @@ -21,10 +21,10 @@ class EnvFile: 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. - + If the value of a variable is None, it will be commented out when it is written to the file. This gives the user a suggestion, but doesn't override values that - may have been set by the user via other means. + may have been set by the user via other means. `path` is the directory where the .env file is located. Defaults to the current working directory. diff --git a/agentstack/tools/__init__.py b/agentstack/tools/__init__.py index 4783fdb4..60e0dffa 100644 --- a/agentstack/tools/__init__.py +++ b/agentstack/tools/__init__.py @@ -12,7 +12,7 @@ class ToolConfig(pydantic.BaseModel): """ This represents the configuration data for a tool. - It parses and validates the `config.json` file and provides a dynamic + It parses and validates the `config.json` file and provides a dynamic interface for interacting with the tool implementation. """ @@ -52,17 +52,21 @@ def type(self) -> type: Dynamically generate a type for the tool module. ie. indicate what methods it's importable module should have. """ + def method_stub(name: str): def not_implemented(*args, **kwargs): - raise NotImplementedError(( + raise NotImplementedError( f"Method '{name}' is configured in config.json for tool '{self.name}'" f"but has not been implemented in the tool module ({self.module_name})." - )) + ) + return not_implemented + # fmt: off type_ = type(f'{snake_to_camel(self.name)}Module', (Protocol,), { # type: ignore[arg-type] method_name: method_stub(method_name) for method_name in self.tools - }) + },) + # fmt: on return runtime_checkable(type_) @property @@ -81,24 +85,24 @@ def module(self) -> ModuleType: assert isinstance(_module, self.type) return _module except AssertionError as e: - raise ValidationError(( + raise ValidationError( f"Tool module `{self.module_name}` does not match the expected implementation. \n" f"The tool's config.json file lists the following public methods: `{'`, `'.join(self.tools)}` " f"but only implements: '{'`, `'.join([m for m in dir(_module) if not m.startswith('_')])}`" - )) + ) except ModuleNotFoundError as e: - raise ValidationError(( + raise ValidationError( f"Could not import tool module: {self.module_name}\n" f"Are you sure you have installed the tool? (agentstack tools add {self.name})\n" f"ModuleNotFoundError: {e}" - )) + ) def get_all_tool_paths() -> list[Path]: """ Get all the paths to the tool configuration files. ie. agentstack/tools// - Tools are identified by having a `config.json` file instide the tools/ directory. + Tools are identified by having a `config.json` file inside the tools/ directory. """ paths = [] tools_dir = get_package_path() / 'tools' diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/tools/agent_connect/__init__.py index 44528a39..31f0caa1 100644 --- a/agentstack/tools/agent_connect/__init__.py +++ b/agentstack/tools/agent_connect/__init__.py @@ -17,16 +17,17 @@ ssl_key_path = os.getenv("AGENT_CONNECT_SSL_KEY_PATH") if not host_domain: - raise Exception(( + raise Exception( "Host domain has not been provided.\n" "Did you set the AGENT_CONNECT_HOST_DOMAIN in you project's .env file?" - )) + ) if not did_document_path: - raise Exception(( + raise Exception( "DID document path has not been provided.\n" "Did you set the AGENT_CONNECT_DID_DOCUMENT_PATH in you project's .env file?" - )) + ) + def generate_did_info(node: SimpleNode, did_document_path: str) -> None: """ diff --git a/agentstack/tools/browserbase/__init__.py b/agentstack/tools/browserbase/__init__.py index 6bd3425e..7edac526 100644 --- a/agentstack/tools/browserbase/__init__.py +++ b/agentstack/tools/browserbase/__init__.py @@ -18,7 +18,7 @@ def load_url( ) -> Any: """ Load a URL in a headless browser and return the page content. - + Args: url: URL to load text_content: Return text content if True, otherwise return raw content diff --git a/agentstack/tools/code_interpreter/__init__.py b/agentstack/tools/code_interpreter/__init__.py index be82f1f7..5b0c9958 100644 --- a/agentstack/tools/code_interpreter/__init__.py +++ b/agentstack/tools/code_interpreter/__init__.py @@ -14,10 +14,12 @@ def _verify_docker_image() -> None: client.images.get(DEFAULT_IMAGE_TAG) except docker.errors.ImageNotFound: if not os.path.exists(DOCKERFILE_PATH): - raise Exception(( - "Dockerfile path has not been provided.\n" - "Did you set the DOCKERFILE_PATH in you project's .env file?" - )) + raise Exception( + ( + "Dockerfile path has not been provided.\n" + "Did you set the DOCKERFILE_PATH in you project's .env file?" + ) + ) client.images.build( path=DOCKERFILE_PATH, @@ -51,16 +53,16 @@ def _init_docker_container() -> docker.models.containers.Container: def run_code(code: str, libraries_used: list[str]) -> str: """ Run the code in a Docker container using Python 3. - + The container will be built and started, the code will be executed, and the container will be stopped. - + Args: code: The code to be executed. ALWAYS PRINT the final result and the output of the code. libraries_used: A list of libraries to be installed in the container before running the code. """ _verify_docker_image() container = _init_docker_container() - + for library in libraries_used: container.exec_run(f"pip install {library}") @@ -68,8 +70,4 @@ def run_code(code: str, libraries_used: list[str]) -> str: container.stop() container.remove() - return ( - f"exit code: {result.exit_code}\n" - f"{result.output.decode('utf-8')}" - ) - + return f"exit code: {result.exit_code}\n" f"{result.output.decode('utf-8')}" diff --git a/agentstack/tools/directory_search/__init__.py b/agentstack/tools/directory_search/__init__.py index 04a53ab6..cf199910 100644 --- a/agentstack/tools/directory_search/__init__.py +++ b/agentstack/tools/directory_search/__init__.py @@ -1,4 +1,5 @@ """Framework-agnostic directory search implementation using embedchain.""" + from typing import Optional from pathlib import Path from embedchain.loaders.directory_loader import DirectoryLoader diff --git a/agentstack/tools/file_read/__init__.py b/agentstack/tools/file_read/__init__.py index 1ab49990..3fca8dcf 100644 --- a/agentstack/tools/file_read/__init__.py +++ b/agentstack/tools/file_read/__init__.py @@ -1,6 +1,7 @@ """ Framework-agnostic implementation of file reading functionality. """ + from typing import Optional from pathlib import Path diff --git a/agentstack/tools/ftp/__init__.py b/agentstack/tools/ftp/__init__.py index 889a6592..3248f551 100644 --- a/agentstack/tools/ftp/__init__.py +++ b/agentstack/tools/ftp/__init__.py @@ -8,34 +8,29 @@ if not HOST: - raise Exception(( - "Host domain has not been provided.\n" - "Did you set the FTP_HOST in you project's .env file?" - )) + raise Exception( + "Host domain has not been provided.\n Did you set the FTP_HOST in you project's .env file?" + ) if not USER: - raise Exception(( - "User has not been provided.\n" - "Did you set the FTP_USER in you project's .env file?" - )) + raise Exception("User has not been provided.\n Did you set the FTP_USER in you project's .env file?") if not PASSWORD: - raise Exception(( - "Password has not been provided.\n" - "Did you set the FTP_PASSWORD in you project's .env file?" - )) + raise Exception( + "Password has not been provided.\n Did you set the FTP_PASSWORD in you project's .env file?" + ) def upload_files(file_paths: list[str]): """ Upload a list of files to the FTP server. - + Args: file_paths: A list of file paths to upload to the FTP server. Returns: bool: True if all files were uploaded successfully, False otherwise. """ - + assert HOST and USER and PASSWORD # appease type checker result = True diff --git a/agentstack/tools/stripe/__init__.py b/agentstack/tools/stripe/__init__.py index ee4bd14d..9c428f83 100644 --- a/agentstack/tools/stripe/__init__.py +++ b/agentstack/tools/stripe/__init__.py @@ -1,116 +1,79 @@ -"""Stripe tool for AgentStack. - -This module provides framework-agnostic functions for interacting with the Stripe API, -supporting payment links, products, and prices operations. -""" - -import json -import os -from typing import Dict, Optional - -import stripe -from stripe_agent_toolkit.functions import ( - create_payment_link as toolkit_create_payment_link, - create_product as toolkit_create_product, - create_price as toolkit_create_price, +from typing import Callable, Optional +import os, sys +from stripe_agent_toolkit.configuration import Configuration, is_tool_allowed +from stripe_agent_toolkit.api import StripeAPI +from stripe_agent_toolkit.tools import tools + +__all__ = [ + "create_payment_link", + "create_product", + "list_products", + "create_price", + "list_prices", +] + +STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY") + +if not STRIPE_SECRET_KEY: + raise Exception( + "Stripe Secret Key not found. Did you set the STRIPE_SECRET_KEY in you project's .env file?" + ) + +_configuration = Configuration( + { + "actions": { + "payment_links": { + "create": True, + }, + "products": { + "create": True, + "read": True, + }, + "prices": { + "create": True, + "read": True, + }, + } + } +) +client = StripeAPI( + secret_key=STRIPE_SECRET_KEY, + context=_configuration.get('context') or None, ) -from stripe_agent_toolkit.configuration import Context -from stripe_agent_toolkit.schema import CreatePaymentLink, CreateProduct, CreatePrice - - -def setup_stripe(secret_key: str) -> None: - """Initialize Stripe with the provided secret key.""" - stripe.api_key = secret_key - stripe.set_app_info("agentstack-stripe", version="0.1.0", url="https://github.com/AgentOps-AI/AgentStack") - - -def create_payment_link(price: str, quantity: int) -> Dict: - """Create a payment link for a specific price and quantity. - - Args: - price: The ID of the price to create a payment link for - quantity: The quantity of items to include in the payment link - - Returns: - Dict containing the payment link ID and URL - """ - # Validate input using toolkit's schema - params = CreatePaymentLink(price=price, quantity=quantity) - return toolkit_create_payment_link(Context(), params.price, params.quantity) - - -def get_payment_link(payment_link_id: str) -> Dict: - """Retrieve a payment link by ID. - - Args: - payment_link_id: The ID of the payment link to retrieve - - Returns: - Dict containing the payment link ID and URL - """ - payment_link = stripe.PaymentLink.retrieve(payment_link_id) - return {"id": payment_link.id, "url": payment_link.url} - - -def create_product(name: str, description: Optional[str] = None) -> Dict: - """Create a new product. - - Args: - name: The name of the product - description: Optional description of the product - - Returns: - Dict containing the product details - """ - # Validate input using toolkit's schema - params = CreateProduct(name=name, description=description) - return toolkit_create_product(Context(), params.name, params.description) - - -def update_product(product_id: str, **kwargs) -> Dict: - """Update an existing product. - - Args: - product_id: The ID of the product to update - **kwargs: Additional fields to update (e.g., name, description) - - Returns: - Dict containing the updated product details - """ - product = stripe.Product.modify(product_id, **kwargs) - return json.loads(str(product)) - - -def create_price(product: str, currency: str, unit_amount: int) -> Dict: - """Create a new price for a product. - - Args: - product: The ID of the product to create a price for - currency: Three-letter ISO currency code - unit_amount: The amount in cents to charge - - Returns: - Dict containing the price details - """ - # Validate input using toolkit's schema - params = CreatePrice(product=product, currency=currency, unit_amount=unit_amount) - return toolkit_create_price(Context(), params.product, params.currency, params.unit_amount) - - -def update_price(price_id: str, **kwargs) -> Dict: - """Update an existing price. - - Args: - price_id: The ID of the price to update - **kwargs: Additional fields to update (e.g., nickname, active) - - Returns: - Dict containing the updated price details - """ - price = stripe.Price.modify(price_id, **kwargs) - return json.loads(str(price)) -# Initialize Stripe when the module is imported if the environment variable is set -if "STRIPE_SECRET_KEY" in os.environ: - setup_stripe(os.environ["STRIPE_SECRET_KEY"]) +def _create_tool_function(tool: dict) -> Callable: + """Dynamically create a tool function based on the tool schema.""" + # `tool` is not typed, but follows this schema: + # { + # "method": "create_customer", + # "name": "Create Customer", + # "description": CREATE_CUSTOMER_PROMPT, + # "args_schema": CreateCustomer, + # "actions": { + # "customers": { + # "create": True, + # } + # }, + # } + schema = tool['args_schema'] + + def func(**kwargs) -> str: + validated_data = schema(**kwargs) + return client.run(tool['method'], **validated_data.dict(exclude_unset=True)) + + func.__name__ = tool['method'] + func.__doc__ = f"{tool['name']}: \n{tool['description']}" + func.__annotations__ = { + 'return': str, + **{name: field.annotation for name, field in schema.model_fields.items()}, + } + return func + + +# Dynamically create tool functions based on the configuration and add them to the module. +for tool in tools: + if not is_tool_allowed(tool, _configuration): + continue + + setattr(sys.modules[__name__], tool['method'], _create_tool_function(tool)) diff --git a/agentstack/tools/stripe/config.json b/agentstack/tools/stripe/config.json index 2cac363e..89b18366 100644 --- a/agentstack/tools/stripe/config.json +++ b/agentstack/tools/stripe/config.json @@ -6,15 +6,15 @@ "STRIPE_SECRET_KEY": null }, "dependencies": [ - "stripe-agent-toolkit>=0.2.0" + "stripe-agent-toolkit==0.2.0", + "stripe>=11.0.0" ], "tools": [ "create_payment_link", - "get_payment_link", "create_product", - "update_product", + "list_products", "create_price", - "update_price" + "list_prices" ], "cta": "🔑 Create your Stripe API key here: https://dashboard.stripe.com/account/apikeys" } \ No newline at end of file diff --git a/agentstack/tools/vision/__init__.py b/agentstack/tools/vision/__init__.py index 55869473..8c6726de 100644 --- a/agentstack/tools/vision/__init__.py +++ b/agentstack/tools/vision/__init__.py @@ -1,4 +1,5 @@ """Vision tool for analyzing images using OpenAI's Vision API.""" + from .vision import analyze_image __all__ = ["analyze_image"] diff --git a/agentstack/tools/vision/vision.py b/agentstack/tools/vision/vision.py index 8d02c958..2cd84503 100644 --- a/agentstack/tools/vision/vision.py +++ b/agentstack/tools/vision/vision.py @@ -27,45 +27,37 @@ def analyze_image(image_path_url: str) -> str: def _analyze_web_image(client: OpenAI, image_path_url: str) -> str: response = client.chat.completions.create( model="gpt-4-vision-preview", - messages=[{ - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - {"type": "image_url", "image_url": {"url": image_path_url}} - ] - }], - max_tokens=300 + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + {"type": "image_url", "image_url": {"url": image_path_url}}, + ], + } + ], + max_tokens=300, ) return response.choices[0].message.content def _analyze_local_image(client: OpenAI, image_path: str) -> str: base64_image = _encode_image(image_path) - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {client.api_key}" - } + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {client.api_key}"} payload = { "model": "gpt-4-vision-preview", - "messages": [{ - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - { - "type": "image_url", - "image_url": { - "url": f"data:image/jpeg;base64,{base64_image}" - } - } - ] - }], - "max_tokens": 300 + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}, + ], + } + ], + "max_tokens": 300, } - response = requests.post( - "https://api.openai.com/v1/chat/completions", - headers=headers, - json=payload - ) + response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) return response.json()["choices"][0]["message"]["content"] diff --git a/pyproject.toml b/pyproject.toml index 343a13b0..d1de5115 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,8 +67,7 @@ exclude = [ "dist", "*.egg-info", "agentstack/templates/", - "examples", - "__init__.py" + "examples" ] line-length = 110 From 4180ff7a9ab4893d95520045caef7080a5dd66ab Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 20 Dec 2024 14:10:02 -0800 Subject: [PATCH 25/34] Removed .vscode/settings.json --- .vscode/settings.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4d4c9664..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "cSpell.words": [ - "agentstack", - "asttools", - "crewai" - ] -} \ No newline at end of file From 812e678289cfb15d44a0ff20f90bfedc125d6531 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 20 Dec 2024 14:13:24 -0800 Subject: [PATCH 26/34] Remove unused toml file. --- agentstack/tools/directory_search/pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 agentstack/tools/directory_search/pyproject.toml diff --git a/agentstack/tools/directory_search/pyproject.toml b/agentstack/tools/directory_search/pyproject.toml deleted file mode 100644 index b575386f..00000000 --- a/agentstack/tools/directory_search/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "agentstack.directory_search" -version = "0.1" - -dependencies = [] - -[project.optional-dependencies] -crewai = [] \ No newline at end of file From 29c956546c7d688e67c00ca173cdccbd010d3c50 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Fri, 20 Dec 2024 14:18:34 -0800 Subject: [PATCH 27/34] Cleanup vision tool package --- agentstack/tools/vision/__init__.py | 67 ++++++++++++++++++++++++++++- agentstack/tools/vision/vision.py | 66 ---------------------------- 2 files changed, 66 insertions(+), 67 deletions(-) delete mode 100644 agentstack/tools/vision/vision.py diff --git a/agentstack/tools/vision/__init__.py b/agentstack/tools/vision/__init__.py index 8c6726de..d6485598 100644 --- a/agentstack/tools/vision/__init__.py +++ b/agentstack/tools/vision/__init__.py @@ -1,5 +1,70 @@ """Vision tool for analyzing images using OpenAI's Vision API.""" -from .vision import analyze_image +import base64 +from typing import Optional +import requests +from openai import OpenAI __all__ = ["analyze_image"] + + +def analyze_image(image_path_url: str) -> str: + """ + Analyze an image using OpenAI's Vision API. + + Args: + image_path_url: Local path or URL to the image + + Returns: + str: Description of the image contents + """ + client = OpenAI() + + if not image_path_url: + return "Image Path or URL is required." + + if "http" in image_path_url: + return _analyze_web_image(client, image_path_url) + return _analyze_local_image(client, image_path_url) + + +def _analyze_web_image(client: OpenAI, image_path_url: str) -> str: + response = client.chat.completions.create( + model="gpt-4-vision-preview", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + {"type": "image_url", "image_url": {"url": image_path_url}}, + ], + } + ], + max_tokens=300, + ) + return response.choices[0].message.content + + +def _analyze_local_image(client: OpenAI, image_path: str) -> str: + base64_image = _encode_image(image_path) + headers = {"Content-Type": "application/json", "Authorization": f"Bearer {client.api_key}"} + payload = { + "model": "gpt-4-vision-preview", + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}, + ], + } + ], + "max_tokens": 300, + } + response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) + return response.json()["choices"][0]["message"]["content"] + + +def _encode_image(image_path: str) -> str: + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") diff --git a/agentstack/tools/vision/vision.py b/agentstack/tools/vision/vision.py deleted file mode 100644 index 2cd84503..00000000 --- a/agentstack/tools/vision/vision.py +++ /dev/null @@ -1,66 +0,0 @@ -import base64 -from typing import Optional -import requests -from openai import OpenAI - - -def analyze_image(image_path_url: str) -> str: - """ - Analyze an image using OpenAI's Vision API. - - Args: - image_path_url: Local path or URL to the image - - Returns: - str: Description of the image contents - """ - client = OpenAI() - - if not image_path_url: - return "Image Path or URL is required." - - if "http" in image_path_url: - return _analyze_web_image(client, image_path_url) - return _analyze_local_image(client, image_path_url) - - -def _analyze_web_image(client: OpenAI, image_path_url: str) -> str: - response = client.chat.completions.create( - model="gpt-4-vision-preview", - messages=[ - { - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - {"type": "image_url", "image_url": {"url": image_path_url}}, - ], - } - ], - max_tokens=300, - ) - return response.choices[0].message.content - - -def _analyze_local_image(client: OpenAI, image_path: str) -> str: - base64_image = _encode_image(image_path) - headers = {"Content-Type": "application/json", "Authorization": f"Bearer {client.api_key}"} - payload = { - "model": "gpt-4-vision-preview", - "messages": [ - { - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}, - ], - } - ], - "max_tokens": 300, - } - response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) - return response.json()["choices"][0]["message"]["content"] - - -def _encode_image(image_path: str) -> str: - with open(image_path, "rb") as image_file: - return base64.b64encode(image_file.read()).decode("utf-8") From daf2ccaab7a1be42620f7e894d663efc46768af7 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Mon, 23 Dec 2024 09:09:09 -0800 Subject: [PATCH 28/34] Prefix internal tools package to avoid confusion with the public `agentstack.tools` package --- MANIFEST.in | 2 +- agentstack/{tools => _tools}/__init__.py | 17 ++++++++++------- .../{tools => _tools}/agent_connect/__init__.py | 0 .../{tools => _tools}/agent_connect/config.json | 0 .../{tools => _tools}/browserbase/__init__.py | 0 .../{tools => _tools}/browserbase/config.json | 0 .../code_interpreter/Dockerfile | 0 .../code_interpreter/__init__.py | 0 .../code_interpreter/config.json | 0 .../{tools => _tools}/composio/__init__.py | 0 .../{tools => _tools}/composio/config.json | 0 .../directory_search/__init__.py | 0 .../directory_search/config.json | 0 agentstack/{tools => _tools}/exa/__init__.py | 0 agentstack/{tools => _tools}/exa/config.json | 0 .../{tools => _tools}/file_read/__init__.py | 0 .../{tools => _tools}/file_read/config.json | 0 .../{tools => _tools}/firecrawl/__init__.py | 0 .../{tools => _tools}/firecrawl/config.json | 0 agentstack/{tools => _tools}/ftp/__init__.py | 0 agentstack/{tools => _tools}/ftp/config.json | 0 agentstack/{tools => _tools}/mem0/__init__.py | 0 agentstack/{tools => _tools}/mem0/config.json | 0 agentstack/{tools => _tools}/neon/__init__.py | 0 agentstack/{tools => _tools}/neon/config.json | 0 .../open_interpreter/__init__.py | 0 .../open_interpreter/config.json | 0 .../{tools => _tools}/perplexity/__init__.py | 0 .../{tools => _tools}/perplexity/config.json | 0 agentstack/{tools => _tools}/stripe/__init__.py | 0 agentstack/{tools => _tools}/stripe/config.json | 0 agentstack/{tools => _tools}/vision/__init__.py | 0 agentstack/{tools => _tools}/vision/config.json | 0 agentstack/{tools => _tools}/~README.md | 0 agentstack/cli/tools.py | 2 +- agentstack/frameworks/__init__.py | 2 +- agentstack/frameworks/crewai.py | 2 +- agentstack/generation/tool_generation.py | 2 +- tests/test_cli_tools.py | 4 ++-- tests/test_frameworks.py | 2 +- tests/test_generation_tool.py | 2 +- tests/test_tool_config.py | 2 +- 42 files changed, 20 insertions(+), 17 deletions(-) rename agentstack/{tools => _tools}/__init__.py (90%) rename agentstack/{tools => _tools}/agent_connect/__init__.py (100%) rename agentstack/{tools => _tools}/agent_connect/config.json (100%) rename agentstack/{tools => _tools}/browserbase/__init__.py (100%) rename agentstack/{tools => _tools}/browserbase/config.json (100%) rename agentstack/{tools => _tools}/code_interpreter/Dockerfile (100%) rename agentstack/{tools => _tools}/code_interpreter/__init__.py (100%) rename agentstack/{tools => _tools}/code_interpreter/config.json (100%) rename agentstack/{tools => _tools}/composio/__init__.py (100%) rename agentstack/{tools => _tools}/composio/config.json (100%) rename agentstack/{tools => _tools}/directory_search/__init__.py (100%) rename agentstack/{tools => _tools}/directory_search/config.json (100%) rename agentstack/{tools => _tools}/exa/__init__.py (100%) rename agentstack/{tools => _tools}/exa/config.json (100%) rename agentstack/{tools => _tools}/file_read/__init__.py (100%) rename agentstack/{tools => _tools}/file_read/config.json (100%) rename agentstack/{tools => _tools}/firecrawl/__init__.py (100%) rename agentstack/{tools => _tools}/firecrawl/config.json (100%) rename agentstack/{tools => _tools}/ftp/__init__.py (100%) rename agentstack/{tools => _tools}/ftp/config.json (100%) rename agentstack/{tools => _tools}/mem0/__init__.py (100%) rename agentstack/{tools => _tools}/mem0/config.json (100%) rename agentstack/{tools => _tools}/neon/__init__.py (100%) rename agentstack/{tools => _tools}/neon/config.json (100%) rename agentstack/{tools => _tools}/open_interpreter/__init__.py (100%) rename agentstack/{tools => _tools}/open_interpreter/config.json (100%) rename agentstack/{tools => _tools}/perplexity/__init__.py (100%) rename agentstack/{tools => _tools}/perplexity/config.json (100%) rename agentstack/{tools => _tools}/stripe/__init__.py (100%) rename agentstack/{tools => _tools}/stripe/config.json (100%) rename agentstack/{tools => _tools}/vision/__init__.py (100%) rename agentstack/{tools => _tools}/vision/config.json (100%) rename agentstack/{tools => _tools}/~README.md (100%) diff --git a/MANIFEST.in b/MANIFEST.in index 44aa8e5a..1f7cc4f8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ recursive-include agentstack/templates * -recursive-include agentstack/tools * +recursive-include agentstack/_tools * include agentstack.json .env .env.example \ No newline at end of file diff --git a/agentstack/tools/__init__.py b/agentstack/_tools/__init__.py similarity index 90% rename from agentstack/tools/__init__.py rename to agentstack/_tools/__init__.py index 60e0dffa..bfde9b18 100644 --- a/agentstack/tools/__init__.py +++ b/agentstack/_tools/__init__.py @@ -9,6 +9,10 @@ from agentstack.utils import get_package_path, open_json_file, term_color, snake_to_camel +TOOLS_DIR: Path = get_package_path() / '_tools' # NOTE: if you change this dir, also update MANIFEST.in +TOOLS_CONFIG_FILENAME: str = 'config.json' + + class ToolConfig(pydantic.BaseModel): """ This represents the configuration data for a tool. @@ -28,7 +32,7 @@ class ToolConfig(pydantic.BaseModel): @classmethod def from_tool_name(cls, name: str) -> 'ToolConfig': - path = get_package_path() / f'tools/{name}/config.json' + path = TOOLS_DIR / name / TOOLS_CONFIG_FILENAME if not os.path.exists(path): # TODO raise exceptions and handle message/exit in cli print(term_color(f'No known agentstack tool: {name}', 'red')) sys.exit(1) @@ -72,7 +76,7 @@ def not_implemented(*args, **kwargs): @property def module_name(self) -> str: """Module name for the tool module.""" - return f"agentstack.tools.{self.name}" + return f"agentstack._tools.{self.name}" @property def module(self) -> ModuleType: @@ -101,14 +105,13 @@ def module(self) -> ModuleType: def get_all_tool_paths() -> list[Path]: """ Get all the paths to the tool configuration files. - ie. agentstack/tools// - Tools are identified by having a `config.json` file inside the tools/ directory. + ie. agentstack/_tools// + Tools are identified by having a `config.json` file inside the _tools/ directory. """ paths = [] - tools_dir = get_package_path() / 'tools' - for tool_dir in tools_dir.iterdir(): + for tool_dir in TOOLS_DIR.iterdir(): if tool_dir.is_dir(): - config_path = tool_dir / 'config.json' + config_path = tool_dir / TOOLS_CONFIG_FILENAME if config_path.exists(): paths.append(tool_dir) return paths diff --git a/agentstack/tools/agent_connect/__init__.py b/agentstack/_tools/agent_connect/__init__.py similarity index 100% rename from agentstack/tools/agent_connect/__init__.py rename to agentstack/_tools/agent_connect/__init__.py diff --git a/agentstack/tools/agent_connect/config.json b/agentstack/_tools/agent_connect/config.json similarity index 100% rename from agentstack/tools/agent_connect/config.json rename to agentstack/_tools/agent_connect/config.json diff --git a/agentstack/tools/browserbase/__init__.py b/agentstack/_tools/browserbase/__init__.py similarity index 100% rename from agentstack/tools/browserbase/__init__.py rename to agentstack/_tools/browserbase/__init__.py diff --git a/agentstack/tools/browserbase/config.json b/agentstack/_tools/browserbase/config.json similarity index 100% rename from agentstack/tools/browserbase/config.json rename to agentstack/_tools/browserbase/config.json diff --git a/agentstack/tools/code_interpreter/Dockerfile b/agentstack/_tools/code_interpreter/Dockerfile similarity index 100% rename from agentstack/tools/code_interpreter/Dockerfile rename to agentstack/_tools/code_interpreter/Dockerfile diff --git a/agentstack/tools/code_interpreter/__init__.py b/agentstack/_tools/code_interpreter/__init__.py similarity index 100% rename from agentstack/tools/code_interpreter/__init__.py rename to agentstack/_tools/code_interpreter/__init__.py diff --git a/agentstack/tools/code_interpreter/config.json b/agentstack/_tools/code_interpreter/config.json similarity index 100% rename from agentstack/tools/code_interpreter/config.json rename to agentstack/_tools/code_interpreter/config.json diff --git a/agentstack/tools/composio/__init__.py b/agentstack/_tools/composio/__init__.py similarity index 100% rename from agentstack/tools/composio/__init__.py rename to agentstack/_tools/composio/__init__.py diff --git a/agentstack/tools/composio/config.json b/agentstack/_tools/composio/config.json similarity index 100% rename from agentstack/tools/composio/config.json rename to agentstack/_tools/composio/config.json diff --git a/agentstack/tools/directory_search/__init__.py b/agentstack/_tools/directory_search/__init__.py similarity index 100% rename from agentstack/tools/directory_search/__init__.py rename to agentstack/_tools/directory_search/__init__.py diff --git a/agentstack/tools/directory_search/config.json b/agentstack/_tools/directory_search/config.json similarity index 100% rename from agentstack/tools/directory_search/config.json rename to agentstack/_tools/directory_search/config.json diff --git a/agentstack/tools/exa/__init__.py b/agentstack/_tools/exa/__init__.py similarity index 100% rename from agentstack/tools/exa/__init__.py rename to agentstack/_tools/exa/__init__.py diff --git a/agentstack/tools/exa/config.json b/agentstack/_tools/exa/config.json similarity index 100% rename from agentstack/tools/exa/config.json rename to agentstack/_tools/exa/config.json diff --git a/agentstack/tools/file_read/__init__.py b/agentstack/_tools/file_read/__init__.py similarity index 100% rename from agentstack/tools/file_read/__init__.py rename to agentstack/_tools/file_read/__init__.py diff --git a/agentstack/tools/file_read/config.json b/agentstack/_tools/file_read/config.json similarity index 100% rename from agentstack/tools/file_read/config.json rename to agentstack/_tools/file_read/config.json diff --git a/agentstack/tools/firecrawl/__init__.py b/agentstack/_tools/firecrawl/__init__.py similarity index 100% rename from agentstack/tools/firecrawl/__init__.py rename to agentstack/_tools/firecrawl/__init__.py diff --git a/agentstack/tools/firecrawl/config.json b/agentstack/_tools/firecrawl/config.json similarity index 100% rename from agentstack/tools/firecrawl/config.json rename to agentstack/_tools/firecrawl/config.json diff --git a/agentstack/tools/ftp/__init__.py b/agentstack/_tools/ftp/__init__.py similarity index 100% rename from agentstack/tools/ftp/__init__.py rename to agentstack/_tools/ftp/__init__.py diff --git a/agentstack/tools/ftp/config.json b/agentstack/_tools/ftp/config.json similarity index 100% rename from agentstack/tools/ftp/config.json rename to agentstack/_tools/ftp/config.json diff --git a/agentstack/tools/mem0/__init__.py b/agentstack/_tools/mem0/__init__.py similarity index 100% rename from agentstack/tools/mem0/__init__.py rename to agentstack/_tools/mem0/__init__.py diff --git a/agentstack/tools/mem0/config.json b/agentstack/_tools/mem0/config.json similarity index 100% rename from agentstack/tools/mem0/config.json rename to agentstack/_tools/mem0/config.json diff --git a/agentstack/tools/neon/__init__.py b/agentstack/_tools/neon/__init__.py similarity index 100% rename from agentstack/tools/neon/__init__.py rename to agentstack/_tools/neon/__init__.py diff --git a/agentstack/tools/neon/config.json b/agentstack/_tools/neon/config.json similarity index 100% rename from agentstack/tools/neon/config.json rename to agentstack/_tools/neon/config.json diff --git a/agentstack/tools/open_interpreter/__init__.py b/agentstack/_tools/open_interpreter/__init__.py similarity index 100% rename from agentstack/tools/open_interpreter/__init__.py rename to agentstack/_tools/open_interpreter/__init__.py diff --git a/agentstack/tools/open_interpreter/config.json b/agentstack/_tools/open_interpreter/config.json similarity index 100% rename from agentstack/tools/open_interpreter/config.json rename to agentstack/_tools/open_interpreter/config.json diff --git a/agentstack/tools/perplexity/__init__.py b/agentstack/_tools/perplexity/__init__.py similarity index 100% rename from agentstack/tools/perplexity/__init__.py rename to agentstack/_tools/perplexity/__init__.py diff --git a/agentstack/tools/perplexity/config.json b/agentstack/_tools/perplexity/config.json similarity index 100% rename from agentstack/tools/perplexity/config.json rename to agentstack/_tools/perplexity/config.json diff --git a/agentstack/tools/stripe/__init__.py b/agentstack/_tools/stripe/__init__.py similarity index 100% rename from agentstack/tools/stripe/__init__.py rename to agentstack/_tools/stripe/__init__.py diff --git a/agentstack/tools/stripe/config.json b/agentstack/_tools/stripe/config.json similarity index 100% rename from agentstack/tools/stripe/config.json rename to agentstack/_tools/stripe/config.json diff --git a/agentstack/tools/vision/__init__.py b/agentstack/_tools/vision/__init__.py similarity index 100% rename from agentstack/tools/vision/__init__.py rename to agentstack/_tools/vision/__init__.py diff --git a/agentstack/tools/vision/config.json b/agentstack/_tools/vision/config.json similarity index 100% rename from agentstack/tools/vision/config.json rename to agentstack/_tools/vision/config.json diff --git a/agentstack/tools/~README.md b/agentstack/_tools/~README.md similarity index 100% rename from agentstack/tools/~README.md rename to agentstack/_tools/~README.md diff --git a/agentstack/cli/tools.py b/agentstack/cli/tools.py index bee8a537..bfd31421 100644 --- a/agentstack/cli/tools.py +++ b/agentstack/cli/tools.py @@ -3,7 +3,7 @@ import inquirer from agentstack.utils import term_color from agentstack import generation -from agentstack.tools import get_all_tools +from agentstack._tools import get_all_tools from agentstack.agents import get_all_agents diff --git a/agentstack/frameworks/__init__.py b/agentstack/frameworks/__init__.py index ee33691b..8ca3d560 100644 --- a/agentstack/frameworks/__init__.py +++ b/agentstack/frameworks/__init__.py @@ -5,7 +5,7 @@ from agentstack import conf from agentstack.exceptions import ValidationError from agentstack.utils import get_framework -from agentstack.tools import ToolConfig +from agentstack._tools import ToolConfig from agentstack.agents import AgentConfig from agentstack.tasks import TaskConfig diff --git a/agentstack/frameworks/crewai.py b/agentstack/frameworks/crewai.py index 973fbfff..911bb728 100644 --- a/agentstack/frameworks/crewai.py +++ b/agentstack/frameworks/crewai.py @@ -3,7 +3,7 @@ import ast from agentstack import conf from agentstack.exceptions import ValidationError -from agentstack.tools import ToolConfig +from agentstack._tools import ToolConfig from agentstack.tasks import TaskConfig from agentstack.agents import AgentConfig from agentstack.generation import asttools diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 69ee7661..1f51f9fe 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -7,7 +7,7 @@ from agentstack import frameworks from agentstack import packaging from agentstack.utils import term_color -from agentstack.tools import ToolConfig +from agentstack._tools import ToolConfig from agentstack.generation.files import EnvFile diff --git a/tests/test_cli_tools.py b/tests/test_cli_tools.py index f5048b72..6b1f0b63 100644 --- a/tests/test_cli_tools.py +++ b/tests/test_cli_tools.py @@ -4,7 +4,7 @@ from parameterized import parameterized from pathlib import Path import shutil -from agentstack.tools import get_all_tool_names +from agentstack._tools import get_all_tool_names BASE_PATH = Path(__file__).parent CLI_ENTRY = [ @@ -35,7 +35,7 @@ def test_add_tool(self, tool_name): result = self._run_cli('init', f"{tool_name}_project") self.assertEqual(result.returncode, 0) os.chdir(self.project_dir / f"{tool_name}_project") - result = self._run_cli('generate', 'agent', 'test_agent', '--llm', 'opeenai/gpt-4o') + result = self._run_cli('generate', 'agent', 'test_agent', '--llm', 'openai/gpt-4o') self.assertEqual(result.returncode, 0) result = self._run_cli('generate', 'task', 'test_task') self.assertEqual(result.returncode, 0) diff --git a/tests/test_frameworks.py b/tests/test_frameworks.py index a469bdaf..0a87a077 100644 --- a/tests/test_frameworks.py +++ b/tests/test_frameworks.py @@ -7,7 +7,7 @@ from agentstack.conf import ConfigFile, set_path from agentstack.exceptions import ValidationError from agentstack import frameworks -from agentstack.tools import ToolConfig +from agentstack._tools import ToolConfig BASE_PATH = Path(__file__).parent diff --git a/tests/test_generation_tool.py b/tests/test_generation_tool.py index c1993389..23247d09 100644 --- a/tests/test_generation_tool.py +++ b/tests/test_generation_tool.py @@ -7,7 +7,7 @@ from agentstack.conf import ConfigFile, set_path from agentstack import frameworks -from agentstack.tools import get_all_tools, ToolConfig +from agentstack._tools import get_all_tools, ToolConfig from agentstack.generation.tool_generation import add_tool, remove_tool diff --git a/tests/test_tool_config.py b/tests/test_tool_config.py index df8e06a6..f86823d4 100644 --- a/tests/test_tool_config.py +++ b/tests/test_tool_config.py @@ -1,7 +1,7 @@ import json import unittest from pathlib import Path -from agentstack.tools import ToolConfig, get_all_tool_paths, get_all_tool_names +from agentstack._tools import ToolConfig, get_all_tool_paths, get_all_tool_names BASE_PATH = Path(__file__).parent From b67a5fadfc23339e694144c6b842c89c8da8b0e2 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 9 Jan 2025 21:22:09 -0800 Subject: [PATCH 29/34] Support removing versioned dependencies when a tool is uninstalled. --- agentstack/generation/tool_generation.py | 6 ++---- agentstack/packaging.py | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 1f51f9fe..0f9b7e0d 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -1,6 +1,5 @@ import os, sys from typing import Optional - from agentstack import conf from agentstack.conf import ConfigFile from agentstack.exceptions import ValidationError @@ -56,9 +55,8 @@ def remove_tool(tool_name: str, agents: Optional[list[str]] = []): # TODO ensure other agents are not using the tool tool = ToolConfig.from_tool_name(tool_name) - if tool.dependencies: - # TODO split on "==", ">=", etc. and only remove by package name - packaging.remove(' '.join(tool.dependencies)) + for dependency in tool.dependencies: + packaging.remove(dependency) # Edit the framework entrypoint file to exclude the tool in the agent definition if not agents: # If no agents are specified, remove the tool from all agents diff --git a/agentstack/packaging.py b/agentstack/packaging.py index b472a51e..0c88a0c2 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -4,6 +4,7 @@ import re import subprocess import select +from packaging.requirements import Requirement from agentstack import conf @@ -56,7 +57,9 @@ def on_error(line: str): def remove(package: str): """Uninstall a package with `uv`.""" - + # If `package` has been provided with a version, it will be stripped. + requirement = Requirement(package) + # TODO it may be worth considering removing unused sub-dependencies as well def on_progress(line: str): if RE_UV_PROGRESS.match(line): @@ -66,7 +69,7 @@ def on_error(line: str): print(f"uv: [error]\n {line.strip()}") _wrap_command_with_callbacks( - [get_uv_bin(), 'remove', '--python', '.venv/bin/python', package], + [get_uv_bin(), 'remove', '--python', '.venv/bin/python', requirement.name], on_progress=on_progress, on_error=on_error, ) From f76541f228844d32cd2552db49b1bf026515a9e5 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 9 Jan 2025 21:26:06 -0800 Subject: [PATCH 30/34] Missing null values for placeholder tool env vars. --- agentstack/_tools/composio/config.json | 2 +- agentstack/_tools/directory_search/config.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agentstack/_tools/composio/config.json b/agentstack/_tools/composio/config.json index cc9c2bd6..cc6bbe7d 100644 --- a/agentstack/_tools/composio/config.json +++ b/agentstack/_tools/composio/config.json @@ -3,7 +3,7 @@ "url": "https://composio.dev/", "category": "unified-apis", "env": { - "COMPOSIO_API_KEY": "..." + "COMPOSIO_API_KEY": null }, "tools": [ "execute_action", diff --git a/agentstack/_tools/directory_search/config.json b/agentstack/_tools/directory_search/config.json index be5d8e2b..b644c20b 100644 --- a/agentstack/_tools/directory_search/config.json +++ b/agentstack/_tools/directory_search/config.json @@ -1,12 +1,12 @@ { "name": "directory_search", "category": "computer-control", - "tools": ["search_directory", "search_fixed_directory"], "description": "Search through files in a directory using embedchain's DirectoryLoader", "env": { - "DIRECTORY_SEARCH_TOOL_PATH": "Optional. Path to directory for fixed directory search." + "DIRECTORY_SEARCH_TOOL_PATH": null, }, "dependencies": [ "embedchain>=0.1.0" - ] + ], + "tools": ["search_directory", "search_fixed_directory"] } From 5ad3cb60548d458400176858ac8e05cfdad8e59d Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 9 Jan 2025 21:33:34 -0800 Subject: [PATCH 31/34] Fix malformed tool config. --- agentstack/_tools/directory_search/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentstack/_tools/directory_search/config.json b/agentstack/_tools/directory_search/config.json index b644c20b..6cbf314e 100644 --- a/agentstack/_tools/directory_search/config.json +++ b/agentstack/_tools/directory_search/config.json @@ -3,7 +3,7 @@ "category": "computer-control", "description": "Search through files in a directory using embedchain's DirectoryLoader", "env": { - "DIRECTORY_SEARCH_TOOL_PATH": null, + "DIRECTORY_SEARCH_TOOL_PATH": null }, "dependencies": [ "embedchain>=0.1.0" From a446f4a9ab4e302722eeea31e2ce4cab60381158 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 9 Jan 2025 21:35:44 -0800 Subject: [PATCH 32/34] Fix typing. --- agentstack/_tools/vision/__init__.py | 2 +- agentstack/generation/tool_generation.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/agentstack/_tools/vision/__init__.py b/agentstack/_tools/vision/__init__.py index d6485598..e491153a 100644 --- a/agentstack/_tools/vision/__init__.py +++ b/agentstack/_tools/vision/__init__.py @@ -42,7 +42,7 @@ def _analyze_web_image(client: OpenAI, image_path_url: str) -> str: ], max_tokens=300, ) - return response.choices[0].message.content + return response.choices[0].message.content # type: ignore[return-value] def _analyze_local_image(client: OpenAI, image_path: str) -> str: diff --git a/agentstack/generation/tool_generation.py b/agentstack/generation/tool_generation.py index 0f9b7e0d..535a5c32 100644 --- a/agentstack/generation/tool_generation.py +++ b/agentstack/generation/tool_generation.py @@ -55,8 +55,9 @@ def remove_tool(tool_name: str, agents: Optional[list[str]] = []): # TODO ensure other agents are not using the tool tool = ToolConfig.from_tool_name(tool_name) - for dependency in tool.dependencies: - packaging.remove(dependency) + if tool.dependencies: + for dependency in tool.dependencies: + packaging.remove(dependency) # Edit the framework entrypoint file to exclude the tool in the agent definition if not agents: # If no agents are specified, remove the tool from all agents From a28b1c4f53c253c7bf8ef7fecd5b79358ba3a46a Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 9 Jan 2025 21:46:50 -0800 Subject: [PATCH 33/34] Add documentation for tool package structures. --- agentstack/_tools/~README.md | 56 -------------------------------- docs/tools/package-structure.mdx | 48 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 56 deletions(-) delete mode 100644 agentstack/_tools/~README.md create mode 100644 docs/tools/package-structure.mdx diff --git a/agentstack/_tools/~README.md b/agentstack/_tools/~README.md deleted file mode 100644 index ff5c6f07..00000000 --- a/agentstack/_tools/~README.md +++ /dev/null @@ -1,56 +0,0 @@ - - -Tool Implementations -==================== -Each tool gets a directory in this folder. - -The directory should contain the following files: - -`config.json` -------------- -This contains the configuration for the tool for use by agentstack. - -`pyproject.toml` ----------------- -This contains the build configuration for the tool and any dependencies. - -`crew.py` ---------- -Python package which contains the tool implementation specific to CrewAI. -Use relative imports to include shared code from the root of the tool's package. -Deploy/install command should handle both single files and directories. - -Additional frameworks will get their own directories. - - -`config.json` Format --------------------- -Tools are configured for installation & removal using JSON files in this directory. - -## Parameters - -### `name` (string) [required] -The name of the tool in snake_case. This is used to identify the tool in the system. - -### `tools` (list) [required] -List of public methods that are accessible in the tool implementation. - -### `tools_bundled` (boolean) [optional] -Indicates that the tool file exports a `list` of tools. Specify the variable name -of the list in the `tools` field. - -### `cta` (string) [optional] -String to print in the terminal when the tool is installed that provides a call to action. - -### `env` (list[dict(str, Any)]) [optional] -Definitions for environment variables that will be appended to the local `.env` file. -This is a list of key-value pairs ie. `[{"ENV_VAR": "value"}, ...]`. -In cases where the user is expected to provide a value, the value is `"..."`. - -### `post_install` (string) [optional] -Shell command that will be executed after packages have been installed and environment -variables have been set. - -### `post_remove` (string) [optional] -Shell command that will be executed after the tool has been removed. - diff --git a/docs/tools/package-structure.mdx b/docs/tools/package-structure.mdx new file mode 100644 index 00000000..57ceb905 --- /dev/null +++ b/docs/tools/package-structure.mdx @@ -0,0 +1,48 @@ + +## Tool Configuration +Each tool gets a directory inside `agentstack/_tools/` where the tool's +source code and configuration will be stored. + +The directory should contain the following files: + +`config.json` +------------- +This contains the configuration for the tool for use by AgentStack, including +metadata, dependencies, configuration & functions exposed by the tool. + +`__init__.py` +--------- +Python package which contains the framework-agnostic tool implementation. Tools +are simple packages which exponse functions; when a tool is loaded into a user's +project, it will be wrapped in the framework-specific tool format by AgentStack. + + +`config.json` Format +-------------------- + +### `name` (string) [required] +The name of the tool in snake_case. This is used to identify the tool in the system. + +### `url` (string) [optional] +The URL of the tool's repository. This is provided to the user to allow them to +learn more about the tool. + +### `category` (string) [required] +The category of the tool. This is used to group tools together in the CLI. + +### `cta` (string) [optional] +String to print in the terminal when the tool is installed that provides a call to action. + +### `env` (list[dict(str, Any)]) [optional] +Definitions for environment variables that will be appended to the local `.env` file. +This is a list of key-value pairs ie. `[{"ENV_VAR": "value"}, ...]`. +In cases where the user is expected to provide their own information, the value is +set to `null` which adds it to the project's `.env` file as a comment. + +### `dependencies` (list[str]) [optional] +List of dependencies that will be installed in the user's project. It is +encouraged that versions are specified, which use the `package>=version` format. + +### `tools` (list[str]) [required] +List of public functions that are accessible in the tool implementation. + From 7ac74548d89c9a2ab7a4a78a7515a9657f94045c Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Mon, 13 Jan 2025 14:51:54 -0800 Subject: [PATCH 34/34] docs for adding tools --- agentstack/_tools/example_config.json | 14 ++++++++++++++ docs/contributing/adding-tools.mdx | 9 +++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 agentstack/_tools/example_config.json diff --git a/agentstack/_tools/example_config.json b/agentstack/_tools/example_config.json new file mode 100644 index 00000000..31bdbd08 --- /dev/null +++ b/agentstack/_tools/example_config.json @@ -0,0 +1,14 @@ +{ + "name": "example_tool", + "url": "https://www.youtube.com/watch?v=xvFZjo5PgG0", + "category": "example_category", + "env": { + "ENV_VAR_1": "foo", + "ENV_VAR_2": "bar" + }, + "dependencies": [ + "dep1>=0.4.20" + ], + "tools": ["function_to_be_exposed", "function_2_to_be_exposed"], + "cta": "A string to be printed after tool install instructing the user on next steps (often a link to implementation docs)" +} diff --git a/docs/contributing/adding-tools.mdx b/docs/contributing/adding-tools.mdx index 53c42b94..040949f0 100644 --- a/docs/contributing/adding-tools.mdx +++ b/docs/contributing/adding-tools.mdx @@ -9,13 +9,14 @@ Adding tools is easy once you understand the project structure. A few things nee - - Create a new tool config at `agentstack/tools/.json` + - Create a new tool config at `agentstack/_tools//config.json` - As an example, look at our [tool config fixture](https://github.com/AgentOps-AI/AgentStack/blob/main/tests/fixtures/tool_config_max.json) - AgentStack uses this to know what code to insert where. Follow the structure to add your tool. - - - In `agentstack/templates//tools`, you'll see other implementations of tools. - - Build your tool implementation for that framework. This file will be inserted in the user's project. + + - In `agentstack/_tools`, you'll see other implementations of tools. + - Create a file `agentstack/_tools//__init__.py`, + - Build your tool implementation simply as python functions in this file. The functions that are to be exposed to the agent as a *tool* should contain detailed docstrings and have typed parameters. - The tools that are exported from this file should be listed in the tool's config json.