Skip to content

Commit 0a8e563

Browse files
authored
Tools filter (#3)
1 parent b7280c1 commit 0a8e563

File tree

24 files changed

+2525
-38
lines changed

24 files changed

+2525
-38
lines changed

.github/workflows/docs-check.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Documentation Check
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "docs/**"
7+
- "mkdocs.yml"
8+
- ".github/workflows/docs*.yml"
9+
10+
jobs:
11+
check:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
- uses: actions/setup-python@v4
16+
with:
17+
python-version: 3.9
18+
19+
- name: Install dependencies
20+
run: |
21+
python -m pip install --upgrade pip
22+
pip install ".[dev]"
23+
24+
- name: Build documentation
25+
run: mkdocs build --strict 2>&1 | tee build-log.txt
26+
27+
- name: Check for warnings
28+
run: |
29+
if grep -q "WARNING" build-log.txt; then
30+
echo "Documentation build produced warnings:"
31+
cat build-log.txt | grep "WARNING"
32+
exit 1
33+
else
34+
echo "Documentation build completed successfully without warnings."
35+
fi

.github/workflows/docs.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Deploy Documentation
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- "docs/**"
9+
- "mkdocs.yml"
10+
- ".github/workflows/docs.yml"
11+
workflow_dispatch: # Allow manual triggering
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
deploy:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v3
21+
- uses: actions/setup-python@v4
22+
with:
23+
python-version: 3.9
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install ".[dev]"
29+
30+
- name: Deploy documentation
31+
run: mkdocs gh-deploy --force

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async def main():
5151
client = AsyncArcade()
5252
# Use the "github" toolkit for this example
5353
# You can use other toolkits like "google", "linkedin", "x", etc.
54-
tools = await get_arcade_tools(client, ["github"])
54+
tools = await get_arcade_tools(client, toolkits=["github"])
5555

5656
# Create an agent that can use the github toolkit
5757
github_agent = Agent(
@@ -66,11 +66,11 @@ async def main():
6666
starting_agent=github_agent,
6767
input="Star the arcadeai/arcade-ai repo",
6868
# make sure you pass a UNIQUE user_id for auth
69-
context={"user_id": "user@example.comiii"},
69+
context={"user_id": "user@example.com"},
7070
)
7171
print("Final output:\n\n", result.final_output)
7272
except AuthorizationError as e:
73-
print("Please Login to Github:", e)
73+
print("Please Login to GitHub:", e)
7474

7575

7676
if __name__ == "__main__":
@@ -113,3 +113,28 @@ Many Arcade tools require user authentication. The authentication flow is manage
113113
## License
114114

115115
This project is licensed under the MIT License - see the LICENSE file for details.
116+
117+
## Documentation
118+
119+
The project documentation is available at [docs.arcadeai.dev/agents-arcade](https://docs.arcadeai.dev/agents-arcade/) and includes:
120+
121+
- Installation instructions
122+
- Quickstart guides
123+
- API reference
124+
- Advanced usage patterns
125+
- Toolkit guides
126+
- Examples
127+
128+
To build and serve the documentation locally:
129+
130+
```bash
131+
# Install development dependencies
132+
pip install -e ".[dev]"
133+
134+
# Serve the documentation
135+
make serve-docs
136+
# or
137+
mkdocs serve
138+
```
139+
140+
Then visit `http://localhost:8000` in your browser.

agents_arcade/_utils.py

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,79 @@
11
import asyncio
22
import json
3+
import os
34
from typing import Any
45

56
from arcadepy import AsyncArcade
67

78

8-
def convert_output_to_json(output: Any) -> str:
9-
if isinstance(output, dict) or isinstance(output, list):
10-
return json.dumps(output)
11-
else:
12-
return str(output)
9+
def get_arcade_client(
10+
base_url: str = "https://api.arcade.dev",
11+
api_key: str = os.getenv("ARCADE_API_KEY", None),
12+
**kwargs: dict[str, Any],
13+
) -> AsyncArcade:
14+
"""
15+
Returns an AsyncArcade client.
16+
"""
17+
if api_key is None:
18+
raise ValueError("ARCADE_API_KEY is not set")
19+
return AsyncArcade(base_url=base_url, api_key=api_key, **kwargs)
20+
21+
22+
async def _get_arcade_tool_formats(
23+
client: AsyncArcade,
24+
tools: list[str] | None = None,
25+
toolkits: list[str] | None = None,
26+
raise_on_empty: bool = True,
27+
) -> list:
28+
"""
29+
Asynchronously fetches tool definitions for each toolkit using client.tools.list,
30+
and returns a list of formatted tools respecting OpenAI's formatting.
31+
32+
Args:
33+
client: AsyncArcade client
34+
tools: Optional list of specific tool names to include.
35+
toolkits: Optional list of toolkit names to include all tools from.
36+
raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
1337
38+
Returns:
39+
A list of formatted tools respecting OpenAI's formatting.
40+
"""
41+
if not tools and not toolkits:
42+
if raise_on_empty:
43+
raise ValueError(
44+
"No tools or toolkits provided to retrieve tool definitions")
45+
return {}
46+
47+
all_tool_formats = []
48+
# Retrieve individual tools if specified
49+
if tools:
50+
tasks = [client.tools.formatted.get(name=tool_id, format="openai")
51+
for tool_id in tools]
52+
responses = await asyncio.gather(*tasks)
53+
for response in responses:
54+
all_tool_formats.append(response)
55+
56+
# Retrieve tools from specified toolkits
57+
if toolkits:
58+
# Create a task for each toolkit to fetch its tool definitions concurrently.
59+
tasks = [client.tools.formatted.list(toolkit=tk, format="openai")
60+
for tk in toolkits]
61+
responses = await asyncio.gather(*tasks)
62+
63+
# Combine the tool definitions from each response.
64+
for response in responses:
65+
# Here we assume the returned response has an "items" attribute
66+
# containing a list of ToolDefinition objects.
67+
all_tool_formats.extend(response.items)
1468

15-
async def get_arcade_client() -> AsyncArcade:
16-
return AsyncArcade()
69+
return all_tool_formats
1770

1871

1972
async def _get_arcade_tool_definitions(
20-
client: AsyncArcade, toolkits: list[str], tools: list[str] | None = None
73+
client: AsyncArcade,
74+
tools: list[str] | None = None,
75+
toolkits: list[str] | None = None,
76+
raise_on_empty: bool = True,
2177
) -> dict[str, bool]:
2278
"""
2379
Asynchronously fetches tool definitions for each toolkit using client.tools.list,
@@ -26,30 +82,55 @@ async def _get_arcade_tool_definitions(
2682
2783
Args:
2884
client: AsyncArcade client
29-
toolkits: List of toolkit names to get tools from
30-
tools: Optional list of specific tool names to include. If None, all tools are included.
85+
tools: Optional list of specific tool names to include.
86+
toolkits: Optional list of toolkit names to include all tools from.
87+
raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
88+
89+
Returns:
90+
A dictionary mapping each tool's name to a boolean indicating whether the
91+
tool requires authorization.
3192
"""
32-
# Create a task for each toolkit to fetch its tool definitions concurrently.
33-
tasks = [client.tools.list(toolkit=toolkit) for toolkit in toolkits]
34-
responses = await asyncio.gather(*tasks)
93+
if not tools and not toolkits:
94+
if raise_on_empty:
95+
raise ValueError(
96+
"No tools or toolkits provided to retrieve tool definitions")
97+
return {}
3598

36-
# Combine the tool definitions from each response.
3799
all_tool_definitions = []
38-
for response in responses:
39-
# Here we assume the returned response has an "items" attribute
40-
# containing a list of ToolDefinition objects.
41-
all_tool_definitions.extend(response.items)
100+
# Retrieve individual tools if specified
101+
if tools:
102+
tasks = [client.tools.get(name=tool_id) for tool_id in tools]
103+
responses = await asyncio.gather(*tasks)
104+
for response in responses:
105+
all_tool_definitions.append(response)
106+
107+
# Retrieve tools from specified toolkits
108+
if toolkits:
109+
# Create a task for each toolkit to fetch its tool definitions concurrently.
110+
tasks = [client.tools.list(toolkit=toolkit) for toolkit in toolkits]
111+
responses = await asyncio.gather(*tasks)
112+
113+
# Combine the tool definitions from each response.
114+
for response in responses:
115+
# Here we assume the returned response has an "items" attribute
116+
# containing a list of ToolDefinition objects.
117+
all_tool_definitions.extend(response.items)
42118

43119
# Create dictionary mapping tool name to a boolean for whether authorization is required.
44120
tool_auth_requirements = {}
45121
for tool_def in all_tool_definitions:
46-
# If tools is None, include all tools
47-
# If tools is not None, only include tools in the list
48-
if tools is None or tool_def.name in tools:
49-
# A tool requires authorization if its requirements exist and its
50-
# authorization is not None.
51-
requires_auth = bool(tool_def.requirements and tool_def.requirements.authorization)
52-
tool_name = "_".join((tool_def.toolkit.name, tool_def.name))
53-
tool_auth_requirements[tool_name] = requires_auth
122+
# A tool requires authorization if its requirements exist and its
123+
# authorization is not None.
124+
requires_auth = bool(
125+
tool_def.requirements and tool_def.requirements.authorization)
126+
tool_name = "_".join((tool_def.toolkit.name, tool_def.name))
127+
tool_auth_requirements[tool_name] = requires_auth
54128

55129
return tool_auth_requirements
130+
131+
132+
def convert_output_to_json(output: Any) -> str:
133+
if isinstance(output, dict) or isinstance(output, list):
134+
return json.dumps(output)
135+
else:
136+
return str(output)

agents_arcade/tools.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import json
22
from functools import partial
3+
from typing import Any
34

45
from agents.run_context import RunContextWrapper
56
from agents.tool import FunctionTool
67
from arcadepy import AsyncArcade
78

89
from agents_arcade._utils import (
910
_get_arcade_tool_definitions,
11+
_get_arcade_tool_formats,
1012
convert_output_to_json,
1113
get_arcade_client,
1214
)
@@ -26,10 +28,12 @@ async def _authorize_tool(client: AsyncArcade, context: RunContextWrapper, tool_
2628

2729

2830
async def _async_invoke_arcade_tool(
29-
context: RunContextWrapper, tool_args: str, tool_name: str, requires_auth: bool
31+
context: RunContextWrapper,
32+
tool_args: str,
33+
tool_name: str,
34+
requires_auth: bool,
35+
client: AsyncArcade,
3036
):
31-
client = await get_arcade_client()
32-
3337
args = json.loads(tool_args)
3438
if requires_auth:
3539
await _authorize_tool(client, context, tool_name)
@@ -47,13 +51,49 @@ async def _async_invoke_arcade_tool(
4751

4852

4953
async def get_arcade_tools(
50-
client: AsyncArcade, toolkits: list[str], tools: list[str] | None = None
54+
client: AsyncArcade | None = None,
55+
tools: list[str] | None = None,
56+
toolkits: list[str] | None = None,
57+
raise_on_empty: bool = True,
58+
**kwargs: dict[str, Any],
5159
) -> list[FunctionTool]:
52-
tool_formats = await client.tools.formatted.list(toolkit=toolkits, format="openai")
53-
auth_spec = await _get_arcade_tool_definitions(client, toolkits, tools)
60+
"""
61+
Asynchronously fetches tool definitions for each toolkit using client.tools.list,
62+
and returns a list of FuntionTool definitions that can be passed to OpenAI
63+
Agents
64+
65+
Args:
66+
client: AsyncArcade client
67+
tools: Optional list of specific tool names to include.
68+
toolkits: Optional list of toolkit names to include all tools from.
69+
raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
70+
kwargs: if a client is not provided, these parameters will initialize it
71+
72+
Returns:
73+
Tool definitions to add to OpenAI's Agent SDK Agents
74+
"""
75+
if not client:
76+
client = get_arcade_client(**kwargs)
77+
78+
if not tools and not toolkits:
79+
if raise_on_empty:
80+
raise ValueError(
81+
"No tools or toolkits provided to retrieve tool definitions")
82+
return {}
83+
84+
tool_formats = await _get_arcade_tool_formats(
85+
client,
86+
tools=tools,
87+
toolkits=toolkits,
88+
raise_on_empty=raise_on_empty)
89+
auth_spec = await _get_arcade_tool_definitions(
90+
client,
91+
tools=tools,
92+
toolkits=toolkits,
93+
raise_on_empty=raise_on_empty)
5494

5595
tool_functions = []
56-
for tool in tool_formats.items:
96+
for tool in tool_formats:
5797
tool_name = tool["function"]["name"]
5898
tool_description = tool["function"]["description"]
5999
tool_params = tool["function"]["parameters"]
@@ -66,6 +106,7 @@ async def get_arcade_tools(
66106
_async_invoke_arcade_tool,
67107
tool_name=tool_name,
68108
requires_auth=requires_auth,
109+
client=client,
69110
),
70111
strict_json_schema=False,
71112
)

0 commit comments

Comments
 (0)