Skip to content

Commit ef1709c

Browse files
committed
Merge branch 'main' into more-coverage
# Conflicts: # agentstack/cli/cli.py
2 parents f5d93b4 + 12732c0 commit ef1709c

File tree

101 files changed

+1984
-918
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+1984
-918
lines changed

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
recursive-include agentstack/templates *
2-
recursive-include agentstack/tools *
2+
recursive-include agentstack/_tools *
33
include agentstack.json .env .env.example

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# AgentStack [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-3100/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
1+
# AgentStack [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-3100/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![python-testing](https://github.com/agentops-ai/agentstack/actions/workflows/python-testing.yml/badge.svg) ![mypy](https://github.com/agentops-ai/agentstack/actions/workflows/mypy.yml/badge.svg) [![codecov.io](https://codecov.io/github/agentops-ai/agentstack/coverage.svg?branch=master)](https://codecov.io/github/agentops-ai/agentstack>?branch=master)
22

33
<img alt="Logo" align="right" src="https://raw.githubusercontent.com/bboynton97/agentstack-docs/3491fe490ea535e7def74c83182dfa8dcfb1f562/logo/dark-sm.svg" width="20%" />
44

@@ -24,9 +24,10 @@ pip install agentstack
2424
agentstack init <project_name>
2525
```
2626

27+
AgentStack scaffolds your _agent stack_ - the tech stack that collectively is your agent
2728

2829
<p align='center'>
29-
<img src='https://raw.githubusercontent.com/agentops-ai/agentstack/main/stack.png' width='600' alt='agentstack init'>
30+
<img src='https://github.com/AgentOps-AI/AgentStack/blob/7b40e53bf7300f69e3291c62d5b45e46ff818245/docs/images/the_agent_stack.png?raw=true' width='600' alt='agentstack init'>
3031
</p>
3132

3233
### Get Started Immediately

agentstack/__init__.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
"""
2-
This it the beginning of the agentstack public API.
2+
This it the beginning of the agentstack public API.
33
44
Methods that have been imported into this file are expected to be used by the
5-
end user inside of their project.
5+
end user inside of their project.
66
"""
7+
8+
from typing import Callable
79
from pathlib import Path
810
from agentstack import conf
911
from agentstack.utils import get_framework
1012
from agentstack.inputs import get_inputs
13+
from agentstack import frameworks
1114

1215
___all___ = [
13-
"conf",
14-
"get_tags",
15-
"get_framework",
16-
"get_inputs",
16+
"conf",
17+
"tools",
18+
"get_tags",
19+
"get_framework",
20+
"get_inputs",
1721
]
1822

1923

@@ -23,3 +27,18 @@ def get_tags() -> list[str]:
2327
"""
2428
return ['agentstack', get_framework(), *conf.get_installed_tools()]
2529

30+
31+
class ToolLoader:
32+
"""
33+
Provides the public interface for accessing tools, wrapped in the
34+
framework-specific callable format.
35+
36+
Get a tool's callables by name with `agentstack.tools[tool_name]`
37+
Include them in your agent's tool list with `tools = [*agentstack.tools[tool_name], ]`
38+
"""
39+
40+
def __getitem__(self, tool_name: str) -> list[Callable]:
41+
return frameworks.get_tool_callables(tool_name)
42+
43+
44+
tools = ToolLoader()

agentstack/_tools/__init__.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from typing import Optional, Protocol, runtime_checkable
2+
from types import ModuleType
3+
import os
4+
import sys
5+
from pathlib import Path
6+
from importlib import import_module
7+
import pydantic
8+
from agentstack.exceptions import ValidationError
9+
from agentstack.utils import get_package_path, open_json_file, term_color, snake_to_camel
10+
11+
12+
TOOLS_DIR: Path = get_package_path() / '_tools' # NOTE: if you change this dir, also update MANIFEST.in
13+
TOOLS_CONFIG_FILENAME: str = 'config.json'
14+
15+
16+
class ToolConfig(pydantic.BaseModel):
17+
"""
18+
This represents the configuration data for a tool.
19+
It parses and validates the `config.json` file and provides a dynamic
20+
interface for interacting with the tool implementation.
21+
"""
22+
23+
name: str
24+
category: str
25+
tools: list[str]
26+
url: Optional[str] = None
27+
cta: Optional[str] = None
28+
env: Optional[dict] = None
29+
dependencies: Optional[list[str]] = None
30+
post_install: Optional[str] = None
31+
post_remove: Optional[str] = None
32+
33+
@classmethod
34+
def from_tool_name(cls, name: str) -> 'ToolConfig':
35+
path = TOOLS_DIR / name / TOOLS_CONFIG_FILENAME
36+
if not os.path.exists(path): # TODO raise exceptions and handle message/exit in cli
37+
print(term_color(f'No known agentstack tool: {name}', 'red'))
38+
sys.exit(1)
39+
return cls.from_json(path)
40+
41+
@classmethod
42+
def from_json(cls, path: Path) -> 'ToolConfig':
43+
data = open_json_file(path)
44+
try:
45+
return cls(**data)
46+
except pydantic.ValidationError as e:
47+
# TODO raise exceptions and handle message/exit in cli
48+
print(term_color(f"Error validating tool config JSON: \n{path}", 'red'))
49+
for error in e.errors():
50+
print(f"{' '.join([str(loc) for loc in error['loc']])}: {error['msg']}")
51+
sys.exit(1)
52+
53+
@property
54+
def type(self) -> type:
55+
"""
56+
Dynamically generate a type for the tool module.
57+
ie. indicate what methods it's importable module should have.
58+
"""
59+
60+
def method_stub(name: str):
61+
def not_implemented(*args, **kwargs):
62+
raise NotImplementedError(
63+
f"Method '{name}' is configured in config.json for tool '{self.name}'"
64+
f"but has not been implemented in the tool module ({self.module_name})."
65+
)
66+
67+
return not_implemented
68+
69+
# fmt: off
70+
type_ = type(f'{snake_to_camel(self.name)}Module', (Protocol,), { # type: ignore[arg-type]
71+
method_name: method_stub(method_name) for method_name in self.tools
72+
},)
73+
# fmt: on
74+
return runtime_checkable(type_)
75+
76+
@property
77+
def module_name(self) -> str:
78+
"""Module name for the tool module."""
79+
return f"agentstack._tools.{self.name}"
80+
81+
@property
82+
def module(self) -> ModuleType:
83+
"""
84+
Import the tool module and validate that it implements the required methods.
85+
Returns the imported module ready for direct use.
86+
"""
87+
try:
88+
_module = import_module(self.module_name)
89+
assert isinstance(_module, self.type)
90+
return _module
91+
except AssertionError as e:
92+
raise ValidationError(
93+
f"Tool module `{self.module_name}` does not match the expected implementation. \n"
94+
f"The tool's config.json file lists the following public methods: `{'`, `'.join(self.tools)}` "
95+
f"but only implements: '{'`, `'.join([m for m in dir(_module) if not m.startswith('_')])}`"
96+
)
97+
except ModuleNotFoundError as e:
98+
raise ValidationError(
99+
f"Could not import tool module: {self.module_name}\n"
100+
f"Are you sure you have installed the tool? (agentstack tools add {self.name})\n"
101+
f"ModuleNotFoundError: {e}"
102+
)
103+
104+
105+
def get_all_tool_paths() -> list[Path]:
106+
"""
107+
Get all the paths to the tool configuration files.
108+
ie. agentstack/_tools/<tool_name>/
109+
Tools are identified by having a `config.json` file inside the _tools/<tool_name> directory.
110+
"""
111+
paths = []
112+
for tool_dir in TOOLS_DIR.iterdir():
113+
if tool_dir.is_dir():
114+
config_path = tool_dir / TOOLS_CONFIG_FILENAME
115+
if config_path.exists():
116+
paths.append(tool_dir)
117+
return paths
118+
119+
120+
def get_all_tool_names() -> list[str]:
121+
return [path.stem for path in get_all_tool_paths()]
122+
123+
124+
def get_all_tools() -> list[ToolConfig]:
125+
return [ToolConfig.from_tool_name(path) for path in get_all_tool_names()]

agentstack/templates/crewai/tools/agent_connect_tool.py renamed to agentstack/_tools/agent_connect/__init__.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
1-
from crewai_tools import tool
2-
from dotenv import load_dotenv
31
import os
4-
5-
from agent_connect.simple_node import SimpleNode
62
import json
3+
from agent_connect.simple_node import SimpleNode
74

8-
load_dotenv()
95

106
# An HTTP and WS service will be started in agent-connect
117
# It can be an IP address or a domain name
12-
host_domain = os.getenv("HOST_DOMAIN")
8+
host_domain = os.getenv("AGENT_CONNECT_HOST_DOMAIN")
139
# Host port, default is 80
14-
host_port = os.getenv("HOST_PORT")
10+
host_port = os.getenv("AGENT_CONNECT_HOST_PORT")
1511
# WS path, default is /ws
16-
host_ws_path = os.getenv("HOST_WS_PATH")
12+
host_ws_path = os.getenv("AGENT_CONNECT_HOST_WS_PATH")
1713
# Path to store DID document
18-
did_document_path = os.getenv("DID_DOCUMENT_PATH")
14+
did_document_path = os.getenv("AGENT_CONNECT_DID_DOCUMENT_PATH")
1915
# SSL certificate path, if using HTTPS, certificate and key need to be provided
20-
ssl_cert_path = os.getenv("SSL_CERT_PATH")
21-
ssl_key_path = os.getenv("SSL_KEY_PATH")
16+
ssl_cert_path = os.getenv("AGENT_CONNECT_SSL_CERT_PATH")
17+
ssl_key_path = os.getenv("AGENT_CONNECT_SSL_KEY_PATH")
18+
19+
if not host_domain:
20+
raise Exception(
21+
"Host domain has not been provided.\n"
22+
"Did you set the AGENT_CONNECT_HOST_DOMAIN in you project's .env file?"
23+
)
24+
25+
if not did_document_path:
26+
raise Exception(
27+
"DID document path has not been provided.\n"
28+
"Did you set the AGENT_CONNECT_DID_DOCUMENT_PATH in you project's .env file?"
29+
)
2230

2331

2432
def generate_did_info(node: SimpleNode, did_document_path: str) -> None:
2533
"""
2634
Generate or load DID information for a node.
27-
35+
2836
Args:
2937
node: SimpleNode instance
3038
did_document_path: Path to store/load DID document
@@ -33,35 +41,33 @@ def generate_did_info(node: SimpleNode, did_document_path: str) -> None:
3341
print(f"Loading existing DID information from {did_document_path}")
3442
with open(did_document_path, "r") as f:
3543
did_info = json.load(f)
36-
node.set_did_info(
37-
did_info["private_key_pem"],
38-
did_info["did"],
39-
did_info["did_document_json"]
40-
)
44+
node.set_did_info(did_info["private_key_pem"], did_info["did"], did_info["did_document_json"])
4145
else:
4246
print("Generating new DID information")
4347
private_key_pem, did, did_document_json = node.generate_did_document()
4448
node.set_did_info(private_key_pem, did, did_document_json)
45-
49+
4650
# Save DID information
47-
os.makedirs(os.path.dirname(did_document_path), exist_ok=True)
51+
if os.path.dirname(did_document_path): # allow saving to current directory
52+
os.makedirs(os.path.dirname(did_document_path), exist_ok=True)
4853
with open(did_document_path, "w") as f:
49-
json.dump({
50-
"private_key_pem": private_key_pem,
51-
"did": did,
52-
"did_document_json": did_document_json
53-
}, f, indent=2)
54+
json.dump(
55+
{"private_key_pem": private_key_pem, "did": did, "did_document_json": did_document_json},
56+
f,
57+
indent=2,
58+
)
5459
print(f"DID information saved to {did_document_path}")
5560

61+
5662
agent_connect_simple_node = SimpleNode(host_domain, host_port, host_ws_path)
5763
generate_did_info(agent_connect_simple_node, did_document_path)
5864
agent_connect_simple_node.run()
5965

60-
@tool("Send Message to Agent by DID")
66+
6167
async def send_message(message: str, destination_did: str) -> bool:
6268
"""
6369
Send a message through agent-connect node.
64-
70+
6571
Args:
6672
message: Message content to be sent
6773
destination_did: DID of the recipient agent
@@ -76,11 +82,11 @@ async def send_message(message: str, destination_did: str) -> bool:
7682
print(f"Failed to send message: {e}")
7783
return False
7884

79-
@tool("Receive Message from Agent")
85+
8086
async def receive_message() -> tuple[str, str]:
8187
"""
8288
Receive message from agent-connect node.
83-
89+
8490
Returns:
8591
tuple[str, str]: Sender DID and received message content, empty string if no message or error occurred
8692
"""
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "agent_connect",
3+
"url": "https://github.com/chgaowei/AgentConnect",
4+
"category": "network-protocols",
5+
"env": {
6+
"AGENT_CONNECT_HOST_DOMAIN": null,
7+
"AGENT_CONNECT_HOST_PORT": 80,
8+
"AGENT_CONNECT_HOST_WS_PATH": "/ws",
9+
"AGENT_CONNECT_DID_DOCUMENT_PATH": "data/agent_connect_did.json",
10+
"AGENT_CONNECT_SSL_CERT_PATH": null,
11+
"AGENT_CONNECT_SSL_KEY_PATH": null
12+
},
13+
"dependencies": [
14+
"agent-connect>=0.3.0"
15+
],
16+
"tools": ["send_message", "receive_message"]
17+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
from typing import Optional, Any
3+
from browserbase import Browserbase
4+
5+
6+
BROWSERBASE_API_KEY = os.getenv("BROWSERBASE_API_KEY")
7+
BROWSERBASE_PROJECT_ID = os.getenv("BROWSERBASE_PROJECT_ID")
8+
9+
client = Browserbase(BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID)
10+
11+
12+
# TODO can we define a type for the return value?
13+
def load_url(
14+
url: str,
15+
text_content: Optional[bool] = True,
16+
session_id: Optional[str] = None,
17+
proxy: Optional[bool] = None,
18+
) -> Any:
19+
"""
20+
Load a URL in a headless browser and return the page content.
21+
22+
Args:
23+
url: URL to load
24+
text_content: Return text content if True, otherwise return raw content
25+
session_id: Session ID to use for the browser
26+
proxy: Use a proxy for the browser
27+
Returns:
28+
Any: Page content
29+
"""
30+
return client.load_url(url)
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
"name": "browserbase",
33
"url": "https://github.com/browserbase/python-sdk",
44
"category": "browsing",
5-
"packages": ["browserbase", "playwright"],
65
"env": {
76
"BROWSERBASE_API_KEY": null,
87
"BROWSERBASE_PROJECT_ID": null
98
},
10-
"tools": ["browserbase"],
9+
"dependencies": [
10+
"browserbase>=1.0.5"
11+
],
12+
"tools": ["load_url"],
1113
"cta": "Create an API key at https://www.browserbase.com/"
1214
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python:3.12-alpine
2+
3+
RUN pip install requests beautifulsoup4
4+
5+
# Set the working directory
6+
WORKDIR /workspace

0 commit comments

Comments
 (0)