arcade-mcp/arcade/arcade/actor/core/base.py
Sam Partee 1c1403d1dd
Arcade Client Implementation: Sync and Async (#22)
This PR introduces both synchronous and asynchronous Arcade client
implementations, providing a robust interface for interacting with the
Arcade API.

## Key Features

1. Synchronous (`SyncArcade`) and Asynchronous (`AsyncArcade`) clients
2. Authentication and Tool resources
3. OpenAI chat completions integration
4. Comprehensive error handling

## Client Methods

Both `SyncArcade` and `AsyncArcade` offer:

- `auth.authorize()`: Initiate authorization
- `auth.poll_authorization()`: Check authorization status
- `tool.run()`: Execute a tool
- `tool.get()`: Retrieve tool specification
- `chat.create()`: Create chat completions

## Usage Examples

### Synchronous Authorization

```python
from arcade.client import AuthProvider, SyncArcade

client = SyncArcade(base_url="https://api.arcade.com", api_key="your_api_key")
auth_response = client.auth.authorize(
    provider=AuthProvider.google,
    scopes=["https://www.googleapis.com/auth/gmail.readonly"],
    user_id="user123"
)
print(f"Authorize at: {auth_response.auth_url}")
```

### Asynchronous Authorization

```python
import asyncio
from arcade.client import AuthProvider, AsyncArcade

async def authorize():
    client = AsyncArcade(base_url="https://api.arcade.com", api_key="your_api_key")
    auth_response = await client.auth.authorize(
        provider=AuthProvider.slack_user,
        scopes=["chat:write", "im:write"],
        user_id="user456"
    )
    print(f"Authorize at: {auth_response.auth_url}")

asyncio.run(authorize())
```

This implementation provides a flexible and powerful way to interact
with Arcade services, supporting both synchronous and asynchronous
workflows.
2024-08-28 17:24:43 -07:00

103 lines
3.2 KiB
Python

import time
from datetime import datetime
from typing import Any, Callable, ClassVar
from arcade.actor.core.common import Actor, Router
from arcade.actor.core.components import (
ActorComponent,
CallToolComponent,
CatalogComponent,
HealthCheckComponent,
)
from arcade.core.catalog import ToolCatalog, Toolkit
from arcade.core.executor import ToolExecutor
from arcade.core.schema import (
ToolCallRequest,
ToolCallResponse,
ToolDefinition,
)
class BaseActor(Actor):
"""
A base actor class that provides a default implementation for registering tools and invoking them.
Actor implementations for specific web frameworks will inherit from this class.
"""
base_path = "/actor" # By default, prefix all our routes with /actor
default_components: ClassVar[tuple[type[ActorComponent], ...]] = (
CatalogComponent,
CallToolComponent,
HealthCheckComponent,
)
def __init__(self) -> None:
"""
Initialize the BaseActor with an empty ToolCatalog.
"""
self.catalog = ToolCatalog()
def get_catalog(self) -> list[ToolDefinition]:
"""
Get the catalog as a list of ToolDefinitions.
"""
return [tool.definition for tool in self.catalog]
def register_tool(self, tool: Callable) -> None:
"""
Register a tool to the catalog.
"""
self.catalog.add_tool(tool)
def register_toolkit(self, toolkit: Toolkit) -> None:
"""
Register a toolkit to the catalog.
"""
self.catalog.add_toolkit(toolkit)
async def call_tool(self, tool_request: ToolCallRequest) -> ToolCallResponse:
"""
Call (invoke) a tool using the ToolExecutor.
"""
tool_name = tool_request.tool.name
tool = self.catalog.get_tool(tool_name)
if not tool:
raise ValueError(f"Tool {tool_name} not found in catalog.")
materialized_tool = self.catalog[tool_name]
start_time = time.time()
output = await ToolExecutor.run(
func=materialized_tool.tool,
definition=materialized_tool.definition,
input_model=materialized_tool.input_model,
output_model=materialized_tool.output_model,
context=tool_request.context,
**tool_request.inputs or {},
)
end_time = time.time() # End time in seconds
duration_ms = (end_time - start_time) * 1000 # Convert to milliseconds
return ToolCallResponse(
invocation_id=tool_request.invocation_id or "",
duration=duration_ms,
finished_at=datetime.now().isoformat(),
success=not output.error,
output=output,
)
def health_check(self) -> dict[str, Any]:
"""
Provide a health check that serves as a heartbeat of actor health.
"""
return {"status": "ok", "tool_count": len(self.catalog.tools.keys())}
def register_routes(self, router: Router) -> None:
"""
Register the necessary routes to the application.
"""
for component_cls in self.default_components:
component_cls(self).register(router)