Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 35 additions & 0 deletions .github/workflows/docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Documentation Check

on:
pull_request:
paths:
- "docs/**"
- "mkdocs.yml"
- ".github/workflows/docs*.yml"

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ".[dev]"

- name: Build documentation
run: mkdocs build --strict 2>&1 | tee build-log.txt

- name: Check for warnings
run: |
if grep -q "WARNING" build-log.txt; then
echo "Documentation build produced warnings:"
cat build-log.txt | grep "WARNING"
exit 1
else
echo "Documentation build completed successfully without warnings."
fi
31 changes: 31 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Deploy Documentation

on:
push:
branches:
- main
paths:
- "docs/**"
- "mkdocs.yml"
- ".github/workflows/docs.yml"
workflow_dispatch: # Allow manual triggering

permissions:
contents: write

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ".[dev]"

- name: Deploy documentation
run: mkdocs gh-deploy --force
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async def main():
client = AsyncArcade()
# Use the "github" toolkit for this example
# You can use other toolkits like "google", "linkedin", "x", etc.
tools = await get_arcade_tools(client, ["github"])
tools = await get_arcade_tools(client, toolkits=["github"])

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


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

This project is licensed under the MIT License - see the LICENSE file for details.

## Documentation

The project documentation is available at [docs.arcadeai.dev/agents-arcade](https://docs.arcadeai.dev/agents-arcade/) and includes:

- Installation instructions
- Quickstart guides
- API reference
- Advanced usage patterns
- Toolkit guides
- Examples

To build and serve the documentation locally:

```bash
# Install development dependencies
pip install -e ".[dev]"

# Serve the documentation
make serve-docs
# or
mkdocs serve
```

Then visit `http://localhost:8000` in your browser.
133 changes: 107 additions & 26 deletions agents_arcade/_utils.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,79 @@
import asyncio
import json
import os
from typing import Any

from arcadepy import AsyncArcade


def convert_output_to_json(output: Any) -> str:
if isinstance(output, dict) or isinstance(output, list):
return json.dumps(output)
else:
return str(output)
def get_arcade_client(
base_url: str = "https://api.arcade.dev",
api_key: str = os.getenv("ARCADE_API_KEY", None),
**kwargs: dict[str, Any],
) -> AsyncArcade:
"""
Returns an AsyncArcade client.
"""
if api_key is None:
raise ValueError("ARCADE_API_KEY is not set")
return AsyncArcade(base_url=base_url, api_key=api_key, **kwargs)


async def _get_arcade_tool_formats(
client: AsyncArcade,
tools: list[str] | None = None,
toolkits: list[str] | None = None,
raise_on_empty: bool = True,
) -> list:
"""
Asynchronously fetches tool definitions for each toolkit using client.tools.list,
and returns a list of formatted tools respecting OpenAI's formatting.

Args:
client: AsyncArcade client
tools: Optional list of specific tool names to include.
toolkits: Optional list of toolkit names to include all tools from.
raise_on_empty: Whether to raise an error if no tools or toolkits are provided.

Returns:
A list of formatted tools respecting OpenAI's formatting.
"""
if not tools and not toolkits:
if raise_on_empty:
raise ValueError(
"No tools or toolkits provided to retrieve tool definitions")
return {}

all_tool_formats = []
# Retrieve individual tools if specified
if tools:
tasks = [client.tools.formatted.get(name=tool_id, format="openai")
for tool_id in tools]
responses = await asyncio.gather(*tasks)
for response in responses:
all_tool_formats.append(response)

# Retrieve tools from specified toolkits
if toolkits:
# Create a task for each toolkit to fetch its tool definitions concurrently.
tasks = [client.tools.formatted.list(toolkit=tk, format="openai")
for tk in toolkits]
responses = await asyncio.gather(*tasks)

# Combine the tool definitions from each response.
for response in responses:
# Here we assume the returned response has an "items" attribute
# containing a list of ToolDefinition objects.
all_tool_formats.extend(response.items)

async def get_arcade_client() -> AsyncArcade:
return AsyncArcade()
return all_tool_formats


async def _get_arcade_tool_definitions(
client: AsyncArcade, toolkits: list[str], tools: list[str] | None = None
client: AsyncArcade,
tools: list[str] | None = None,
toolkits: list[str] | None = None,
raise_on_empty: bool = True,
) -> dict[str, bool]:
"""
Asynchronously fetches tool definitions for each toolkit using client.tools.list,
Expand All @@ -26,30 +82,55 @@ async def _get_arcade_tool_definitions(

Args:
client: AsyncArcade client
toolkits: List of toolkit names to get tools from
tools: Optional list of specific tool names to include. If None, all tools are included.
tools: Optional list of specific tool names to include.
toolkits: Optional list of toolkit names to include all tools from.
raise_on_empty: Whether to raise an error if no tools or toolkits are provided.

Returns:
A dictionary mapping each tool's name to a boolean indicating whether the
tool requires authorization.
"""
# Create a task for each toolkit to fetch its tool definitions concurrently.
tasks = [client.tools.list(toolkit=toolkit) for toolkit in toolkits]
responses = await asyncio.gather(*tasks)
if not tools and not toolkits:
if raise_on_empty:
raise ValueError(
"No tools or toolkits provided to retrieve tool definitions")
return {}

# Combine the tool definitions from each response.
all_tool_definitions = []
for response in responses:
# Here we assume the returned response has an "items" attribute
# containing a list of ToolDefinition objects.
all_tool_definitions.extend(response.items)
# Retrieve individual tools if specified
if tools:
tasks = [client.tools.get(name=tool_id) for tool_id in tools]
responses = await asyncio.gather(*tasks)
for response in responses:
all_tool_definitions.append(response)

# Retrieve tools from specified toolkits
if toolkits:
# Create a task for each toolkit to fetch its tool definitions concurrently.
tasks = [client.tools.list(toolkit=toolkit) for toolkit in toolkits]
responses = await asyncio.gather(*tasks)

# Combine the tool definitions from each response.
for response in responses:
# Here we assume the returned response has an "items" attribute
# containing a list of ToolDefinition objects.
all_tool_definitions.extend(response.items)

# Create dictionary mapping tool name to a boolean for whether authorization is required.
tool_auth_requirements = {}
for tool_def in all_tool_definitions:
# If tools is None, include all tools
# If tools is not None, only include tools in the list
if tools is None or tool_def.name in tools:
# A tool requires authorization if its requirements exist and its
# authorization is not None.
requires_auth = bool(tool_def.requirements and tool_def.requirements.authorization)
tool_name = "_".join((tool_def.toolkit.name, tool_def.name))
tool_auth_requirements[tool_name] = requires_auth
# A tool requires authorization if its requirements exist and its
# authorization is not None.
requires_auth = bool(
tool_def.requirements and tool_def.requirements.authorization)
tool_name = "_".join((tool_def.toolkit.name, tool_def.name))
tool_auth_requirements[tool_name] = requires_auth

return tool_auth_requirements


def convert_output_to_json(output: Any) -> str:
if isinstance(output, dict) or isinstance(output, list):
return json.dumps(output)
else:
return str(output)
55 changes: 48 additions & 7 deletions agents_arcade/tools.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import json
from functools import partial
from typing import Any

from agents.run_context import RunContextWrapper
from agents.tool import FunctionTool
from arcadepy import AsyncArcade

from agents_arcade._utils import (
_get_arcade_tool_definitions,
_get_arcade_tool_formats,
convert_output_to_json,
get_arcade_client,
)
Expand All @@ -26,10 +28,12 @@ async def _authorize_tool(client: AsyncArcade, context: RunContextWrapper, tool_


async def _async_invoke_arcade_tool(
context: RunContextWrapper, tool_args: str, tool_name: str, requires_auth: bool
context: RunContextWrapper,
tool_args: str,
tool_name: str,
requires_auth: bool,
client: AsyncArcade,
):
client = await get_arcade_client()

args = json.loads(tool_args)
if requires_auth:
await _authorize_tool(client, context, tool_name)
Expand All @@ -47,13 +51,49 @@ async def _async_invoke_arcade_tool(


async def get_arcade_tools(
client: AsyncArcade, toolkits: list[str], tools: list[str] | None = None
client: AsyncArcade | None = None,
tools: list[str] | None = None,
toolkits: list[str] | None = None,
raise_on_empty: bool = True,
**kwargs: dict[str, Any],
) -> list[FunctionTool]:
tool_formats = await client.tools.formatted.list(toolkit=toolkits, format="openai")
auth_spec = await _get_arcade_tool_definitions(client, toolkits, tools)
"""
Asynchronously fetches tool definitions for each toolkit using client.tools.list,
and returns a list of FuntionTool definitions that can be passed to OpenAI
Agents

Args:
client: AsyncArcade client
tools: Optional list of specific tool names to include.
toolkits: Optional list of toolkit names to include all tools from.
raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
kwargs: if a client is not provided, these parameters will initialize it

Returns:
Tool definitions to add to OpenAI's Agent SDK Agents
"""
if not client:
client = get_arcade_client(**kwargs)

if not tools and not toolkits:
if raise_on_empty:
raise ValueError(
"No tools or toolkits provided to retrieve tool definitions")
return {}

tool_formats = await _get_arcade_tool_formats(
client,
tools=tools,
toolkits=toolkits,
raise_on_empty=raise_on_empty)
auth_spec = await _get_arcade_tool_definitions(
client,
tools=tools,
toolkits=toolkits,
raise_on_empty=raise_on_empty)

tool_functions = []
for tool in tool_formats.items:
for tool in tool_formats:
tool_name = tool["function"]["name"]
tool_description = tool["function"]["description"]
tool_params = tool["function"]["parameters"]
Expand All @@ -66,6 +106,7 @@ async def get_arcade_tools(
_async_invoke_arcade_tool,
tool_name=tool_name,
requires_auth=requires_auth,
client=client,
),
strict_json_schema=False,
)
Expand Down
Loading