Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3b2e988
Tool packaging abstractions. Needs tests.
tcdent Nov 30, 2024
df7dfc4
Merge branch 'main' into tool-packages
tcdent Dec 2, 2024
4982c14
Remove `packages` from ToolConfig tests.
tcdent Dec 2, 2024
3c7568e
Tool package init file placeholder.
tcdent Dec 2, 2024
bdbd905
Bug in ToolConfig.get_path
tcdent Dec 2, 2024
02d61f6
Merge cruft in tool_generation
tcdent Dec 2, 2024
e36933c
Merge branch 'main' into tool-packages
tcdent Dec 4, 2024
610dc2a
Reformat tool packages to new format.
tcdent Dec 4, 2024
a94cf40
Merge branch 'main' into tool-packages
tcdent Dec 5, 2024
af8d00c
Merge branch 'main' into tool-packages
tcdent Dec 18, 2024
b85f089
Tools now get wrapped in a framework-specific layer to allow for our …
tcdent Dec 18, 2024
dffff68
EnvFile now prefixes empty variables with a comment (#) to avoid over…
tcdent Dec 18, 2024
27b78fc
refactor: make directory_search tool framework-agnostic
devin-ai-integration[bot] Dec 18, 2024
c403690
refactor: make directory_search tool framework-agnostic
devin-ai-integration[bot] Dec 18, 2024
7c75b9c
chore: add embedchain dependency to directory_search tool
devin-ai-integration[bot] Dec 18, 2024
2f6c787
refactor: make composio tool framework-agnostic
devin-ai-integration[bot] Dec 19, 2024
1c62686
chore: restore cta field in composio tool config
devin-ai-integration[bot] Dec 19, 2024
459474d
refactor: make vision tool framework-agnostic
devin-ai-integration[bot] Dec 19, 2024
d60da5b
refactor: namespace agent_connect environment variables with AGENT_CO…
devin-ai-integration[bot] Dec 19, 2024
8a24f14
refactor: namespace agent_connect environment variables with AGENT_CO…
devin-ai-integration[bot] Dec 19, 2024
6664c20
refactor: resolve merge conflicts and namespace agent_connect environ…
devin-ai-integration[bot] Dec 19, 2024
e99eaa1
Prefix environment variables in agent_connect tool
tcdent Dec 19, 2024
9933cad
refactor: make file_read tool framework-agnostic
devin-ai-integration[bot] Dec 19, 2024
76be215
refactor: move directory_search implementation to __init__.py
devin-ai-integration[bot] Dec 19, 2024
4f4cf5e
refactor: make stripe tool framework-agnostic
devin-ai-integration[bot] Dec 19, 2024
5da4248
refactor: use stripe_agent_toolkit functions for stripe tool
devin-ai-integration[bot] Dec 19, 2024
e3911dd
Fix stripe config.json
tcdent Dec 20, 2024
b01d49f
Delete agentstack/tools/file_read/pyproject.toml
tcdent Dec 20, 2024
2195c9e
Reimplement stripe tool. Add __init__.py files to linter.
tcdent Dec 20, 2024
4180ff7
Removed .vscode/settings.json
tcdent Dec 20, 2024
639486a
Merge branch 'tcdent-gitignore' into tool-packages
tcdent Dec 20, 2024
812e678
Remove unused toml file.
tcdent Dec 20, 2024
29c9565
Cleanup vision tool package
tcdent Dec 20, 2024
daf2cca
Prefix internal tools package to avoid confusion with the public `age…
tcdent Dec 23, 2024
54572e0
Merge branch 'main' into tool-packages
tcdent Jan 10, 2025
b67a5fa
Support removing versioned dependencies when a tool is uninstalled.
tcdent Jan 10, 2025
f76541f
Missing null values for placeholder tool env vars.
tcdent Jan 10, 2025
5ad3cb6
Fix malformed tool config.
tcdent Jan 10, 2025
a446f4a
Fix typing.
tcdent Jan 10, 2025
a28b1c4
Add documentation for tool package structures.
tcdent Jan 10, 2025
7ac7454
docs for adding tools
bboynton97 Jan 13, 2025
279dbd6
Merge branch 'main' into tool-packages
bboynton97 Jan 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
recursive-include agentstack/templates *
recursive-include agentstack/tools *
recursive-include agentstack/_tools *
include agentstack.json .env .env.example
31 changes: 25 additions & 6 deletions agentstack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
"""
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
from agentstack.utils import get_framework
from agentstack.inputs import get_inputs
from agentstack import frameworks

___all___ = [
"conf",
"get_tags",
"get_framework",
"get_inputs",
"conf",
"tools",
"get_tags",
"get_framework",
"get_inputs",
]


Expand All @@ -23,3 +27,18 @@ 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()
125 changes: 125 additions & 0 deletions agentstack/_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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


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.
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 = 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)
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

# 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
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/<tool_name>/
Tools are identified by having a `config.json` file inside the _tools/<tool_name> directory.
"""
paths = []
for tool_dir in TOOLS_DIR.iterdir():
if tool_dir.is_dir():
config_path = tool_dir / TOOLS_CONFIG_FILENAME
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()]
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
from crewai_tools import tool
from dotenv import load_dotenv
import os

from agent_connect.simple_node import SimpleNode
import json
from agent_connect.simple_node import SimpleNode

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_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 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?"
)


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
Expand All @@ -33,35 +41,33 @@ def generate_did_info(node: SimpleNode, did_document_path: str) -> None:
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"]
)
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)
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)
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()

@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
Expand All @@ -76,11 +82,11 @@ async def send_message(message: str, destination_did: str) -> bool:
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
"""
Expand Down
17 changes: 17 additions & 0 deletions agentstack/_tools/agent_connect/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "agent_connect",
"url": "https://github.com/chgaowei/AgentConnect",
"category": "network-protocols",
"env": {
"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"
],
"tools": ["send_message", "receive_message"]
}
30 changes: 30 additions & 0 deletions agentstack/_tools/browserbase/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
"name": "browserbase",
"url": "https://github.com/browserbase/python-sdk",
"category": "browsing",
"packages": ["browserbase", "playwright"],
"env": {
"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/"
}
6 changes: 6 additions & 0 deletions agentstack/_tools/code_interpreter/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.12-alpine

RUN pip install requests beautifulsoup4

# Set the working directory
WORKDIR /workspace
Loading
Loading