Replace arcade.client with arcadepy (#119)
Closes: https://app.clickup.com/t/86b2k2962 --------- Co-authored-by: sdreyer <sterling@arcade-ai.com>
This commit is contained in:
parent
fd8190e216
commit
9d00295e33
18 changed files with 132 additions and 1391 deletions
|
|
@ -8,6 +8,8 @@ from typing import Any, Optional
|
|||
from urllib.parse import urlencode
|
||||
|
||||
import typer
|
||||
from arcadepy import Arcade
|
||||
from arcadepy.types import AuthorizationResponse
|
||||
from openai import OpenAI, OpenAIError
|
||||
from rich.console import Console
|
||||
from rich.markup import escape
|
||||
|
|
@ -36,7 +38,6 @@ from arcade.cli.utils import (
|
|||
log_engine_health,
|
||||
validate_and_get_config,
|
||||
)
|
||||
from arcade.client import Arcade
|
||||
|
||||
cli = typer.Typer(
|
||||
cls=OrderCommands,
|
||||
|
|
@ -260,7 +261,8 @@ def chat(
|
|||
history.append({"role": "user", "content": user_input})
|
||||
|
||||
try:
|
||||
openai_client = OpenAI(api_key=config.api.key, base_url=config.engine_url)
|
||||
# TODO fixup configuration to remove this + "/v1" workaround
|
||||
openai_client = OpenAI(api_key=config.api.key, base_url=config.engine_url + "/v1")
|
||||
chat_result = handle_chat_interaction(
|
||||
openai_client, model, history, user_email, stream
|
||||
)
|
||||
|
|
@ -273,7 +275,7 @@ def chat(
|
|||
if tool_authorization and is_authorization_pending(tool_authorization):
|
||||
chat_result = handle_tool_authorization(
|
||||
client,
|
||||
tool_authorization,
|
||||
AuthorizationResponse.model_validate(tool_authorization),
|
||||
history,
|
||||
openai_client,
|
||||
model,
|
||||
|
|
@ -402,7 +404,7 @@ def evals(
|
|||
|
||||
# Try to hit /health endpoint on engine and warn if it is down
|
||||
with Arcade(api_key=config.api.key, base_url=config.engine_url) as client:
|
||||
log_engine_health(client) # type: ignore[arg-type]
|
||||
log_engine_health(client)
|
||||
|
||||
# Use the new function to load eval suites
|
||||
eval_suites = load_eval_suites(eval_files)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import importlib.util
|
|||
import webbrowser
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Union
|
||||
from typing import Any, Callable, Union, cast
|
||||
|
||||
import typer
|
||||
from arcadepy import NOT_GIVEN, APIConnectionError, APIStatusError, APITimeoutError, Arcade
|
||||
from arcadepy.types import AuthorizationResponse
|
||||
from openai import OpenAI
|
||||
from openai.resources.chat.completions import ChatCompletionChunk, Stream
|
||||
from openai.types.chat.chat_completion import Choice as ChatCompletionChoice
|
||||
|
|
@ -16,9 +18,6 @@ from rich.text import Text
|
|||
from typer.core import TyperGroup
|
||||
from typer.models import Context
|
||||
|
||||
from arcade.client.client import Arcade
|
||||
from arcade.client.errors import APITimeoutError, EngineNotHealthyError, EngineOfflineError
|
||||
from arcade.client.schema import AuthResponse
|
||||
from arcade.core.catalog import ToolCatalog
|
||||
from arcade.core.config_model import Config
|
||||
from arcade.core.errors import ToolkitLoadError
|
||||
|
|
@ -96,7 +95,14 @@ def get_tools_from_engine(
|
|||
) -> list[ToolDefinition]:
|
||||
config = get_config_with_overrides(force_tls, force_no_tls, host, port)
|
||||
client = Arcade(api_key=config.api.key, base_url=config.engine_url)
|
||||
return client.tools.list_tools(toolkit=toolkit)
|
||||
|
||||
tools = []
|
||||
# TODO: This is a hack! limit=100 is a workaround for broken(?) pagination in Stainless
|
||||
for page in client.tools.list(limit=100, toolkit=toolkit or NOT_GIVEN).iter_pages():
|
||||
for item in page:
|
||||
tools.append(ToolDefinition.model_validate(item.model_dump()))
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def get_tool_messages(choice: dict) -> list[dict]:
|
||||
|
|
@ -231,9 +237,22 @@ def apply_config_overrides(
|
|||
|
||||
def log_engine_health(client: Arcade) -> None:
|
||||
try:
|
||||
client.health.check()
|
||||
result = client.health.check(timeout=2)
|
||||
if result.healthy:
|
||||
return
|
||||
|
||||
except EngineNotHealthyError as e:
|
||||
console.print(
|
||||
"⚠️ Warning: Arcade Engine is unhealthy",
|
||||
style="bold yellow",
|
||||
)
|
||||
|
||||
except APIConnectionError:
|
||||
console.print(
|
||||
"⚠️ Warning: Arcade Engine was unreachable. (Is it running?)",
|
||||
style="bold yellow",
|
||||
)
|
||||
|
||||
except APIStatusError as e:
|
||||
console.print(
|
||||
"[bold][yellow]⚠️ Warning: "
|
||||
+ str(e)
|
||||
|
|
@ -244,11 +263,6 @@ def log_engine_health(client: Arcade) -> None:
|
|||
+ "[/red]"
|
||||
+ "[yellow])[/yellow][/bold]"
|
||||
)
|
||||
except EngineOfflineError:
|
||||
console.print(
|
||||
"⚠️ Warning: Arcade Engine was unreachable. (Is it running?)",
|
||||
style="bold yellow",
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -319,7 +333,7 @@ def handle_chat_interaction(
|
|||
|
||||
def handle_tool_authorization(
|
||||
arcade_client: Arcade,
|
||||
tool_authorization: dict,
|
||||
tool_authorization: AuthorizationResponse,
|
||||
history: list[dict[str, Any]],
|
||||
openai_client: OpenAI,
|
||||
model: str,
|
||||
|
|
@ -327,8 +341,8 @@ def handle_tool_authorization(
|
|||
stream: bool,
|
||||
) -> ChatInteractionResult:
|
||||
with Live(console=console, refresh_per_second=4) as live:
|
||||
if "authorization_url" in tool_authorization:
|
||||
authorization_url = str(tool_authorization["authorization_url"])
|
||||
if tool_authorization.authorization_url:
|
||||
authorization_url = str(tool_authorization.authorization_url)
|
||||
webbrowser.open(authorization_url)
|
||||
message = (
|
||||
"You'll need to authorize this action in your browser.\n\n"
|
||||
|
|
@ -346,18 +360,25 @@ def handle_tool_authorization(
|
|||
return handle_chat_interaction(openai_client, model, history, user_email, stream)
|
||||
|
||||
|
||||
def wait_for_authorization_completion(client: Arcade, tool_authorization: dict | None) -> None:
|
||||
def wait_for_authorization_completion(
|
||||
client: Arcade, tool_authorization: AuthorizationResponse | None
|
||||
) -> None:
|
||||
"""
|
||||
Wait for the authorization for a tool call to complete i.e., wait for the user to click on
|
||||
the approval link and authorize Arcade.
|
||||
"""
|
||||
if tool_authorization is None:
|
||||
return
|
||||
auth_response = AuthResponse.model_validate(tool_authorization)
|
||||
|
||||
auth_response = AuthorizationResponse.model_validate(tool_authorization)
|
||||
|
||||
while auth_response.status != "completed":
|
||||
try:
|
||||
auth_response = client.auth.status(auth_response, wait=60)
|
||||
auth_response = client.auth.status(
|
||||
authorization_id=cast(str, auth_response.authorization_id),
|
||||
scopes=" ".join(auth_response.scopes) if auth_response.scopes else NOT_GIVEN,
|
||||
wait=59,
|
||||
)
|
||||
except APITimeoutError:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
from arcade.client.client import Arcade, AsyncArcade
|
||||
from arcade.client.schema import AuthProvider
|
||||
|
||||
__all__ = [
|
||||
"AuthProvider",
|
||||
"AsyncArcade",
|
||||
"Arcade",
|
||||
]
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
from typing import Any, Generic, TypeVar
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import httpx
|
||||
from httpx import Timeout
|
||||
|
||||
from arcade.client.errors import (
|
||||
APITimeoutError,
|
||||
BadRequestError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
PermissionDeniedError,
|
||||
RateLimitError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
ResponseT = TypeVar("ResponseT")
|
||||
|
||||
|
||||
class BaseResource(Generic[T]):
|
||||
"""Base class for all resources."""
|
||||
|
||||
_path: str = ""
|
||||
_version: str = "v1"
|
||||
|
||||
def __init__(self, client: T) -> None:
|
||||
self._client = client
|
||||
self._resource_path = urljoin(
|
||||
self._client._base_url, # type: ignore[attr-defined]
|
||||
f"{self._version}/{self._path}",
|
||||
)
|
||||
|
||||
|
||||
class BaseArcadeClient:
|
||||
"""Base class for Arcade clients."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
timeout: float | Timeout = 30.0,
|
||||
retries: int = 1,
|
||||
):
|
||||
"""
|
||||
Initialize the BaseArcadeClient.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for the Arcade API.
|
||||
api_key: The API key for authentication.
|
||||
headers: Additional headers to include in requests.
|
||||
timeout: Request timeout in seconds.
|
||||
retries: Number of retries for failed requests.
|
||||
"""
|
||||
if base_url is None or api_key is None:
|
||||
from arcade.core.config import config
|
||||
|
||||
base_url = base_url or config.engine_url
|
||||
api_key = api_key or config.api.key
|
||||
self._base_url = base_url
|
||||
self._api_key = api_key
|
||||
|
||||
self._headers = headers or {}
|
||||
self._headers.setdefault("Authorization", f"Bearer {self._api_key}")
|
||||
self._headers.setdefault("Content-Type", "application/json")
|
||||
self._timeout = timeout
|
||||
self._retries = retries
|
||||
|
||||
def _build_url(self, path: str) -> str:
|
||||
"""
|
||||
Build the full URL for a given path.
|
||||
"""
|
||||
return urljoin(self._base_url, path)
|
||||
|
||||
def _handle_http_error(self, e: httpx.HTTPStatusError) -> None:
|
||||
error_map = {
|
||||
400: BadRequestError,
|
||||
401: UnauthorizedError,
|
||||
403: PermissionDeniedError,
|
||||
404: NotFoundError,
|
||||
408: APITimeoutError,
|
||||
429: RateLimitError,
|
||||
500: InternalServerError,
|
||||
}
|
||||
status_code = e.response.status_code
|
||||
error_class = error_map.get(status_code, InternalServerError)
|
||||
msg = e.response.json()
|
||||
if isinstance(msg, dict) and "error" in msg:
|
||||
raise error_class(msg["error"], response=e.response) from None
|
||||
raise error_class(msg, response=e.response) from None
|
||||
|
||||
|
||||
class SyncArcadeClient(BaseArcadeClient):
|
||||
"""Synchronous Arcade client."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._client = httpx.Client(
|
||||
base_url=self._base_url,
|
||||
headers=self._headers,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response: # type: ignore[return]
|
||||
"""
|
||||
Make a synchronous HTTP request.
|
||||
"""
|
||||
url = self._build_url(path)
|
||||
for attempt in range(self._retries):
|
||||
try:
|
||||
response = self._client.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response # noqa: TRY300
|
||||
except httpx.HTTPStatusError as e:
|
||||
if attempt == self._retries - 1:
|
||||
self._handle_http_error(e)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the client session."""
|
||||
self._client.close()
|
||||
|
||||
def __enter__(self) -> "SyncArcadeClient":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
self.close()
|
||||
|
||||
|
||||
class AsyncArcadeClient(BaseArcadeClient):
|
||||
"""Asynchronous Arcade client."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
async def _get_client(self) -> httpx.AsyncClient:
|
||||
"""
|
||||
Get or create an asynchronous HTTP client.
|
||||
"""
|
||||
if self._client is None:
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self._base_url,
|
||||
headers=self._headers,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
return self._client
|
||||
|
||||
async def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response: # type: ignore[return]
|
||||
"""
|
||||
Make an asynchronous HTTP request.
|
||||
"""
|
||||
client = await self._get_client()
|
||||
url = self._build_url(path)
|
||||
for attempt in range(self._retries):
|
||||
try:
|
||||
response = await client.request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
return response # noqa: TRY300
|
||||
except httpx.HTTPStatusError as e:
|
||||
if attempt == self._retries - 1:
|
||||
self._handle_http_error(e)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the client session."""
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
|
||||
async def __aenter__(self) -> "AsyncArcadeClient":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
await self.close()
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
import json
|
||||
from typing import Any, TypeVar, Union
|
||||
|
||||
from httpx import Timeout
|
||||
|
||||
from arcade.client.base import (
|
||||
AsyncArcadeClient,
|
||||
BaseResource,
|
||||
SyncArcadeClient,
|
||||
)
|
||||
from arcade.client.errors import APIStatusError, EngineNotHealthyError, EngineOfflineError
|
||||
from arcade.client.schema import (
|
||||
AuthProvider,
|
||||
AuthProviderType,
|
||||
AuthRequest,
|
||||
AuthResponse,
|
||||
ExecuteToolResponse,
|
||||
HealthCheckResponse,
|
||||
)
|
||||
from arcade.core.schema import ToolDefinition
|
||||
|
||||
T = TypeVar("T")
|
||||
ClientT = TypeVar("ClientT", SyncArcadeClient, AsyncArcadeClient)
|
||||
|
||||
|
||||
class AuthResource(BaseResource[ClientT]):
|
||||
"""Authentication resource."""
|
||||
|
||||
_path = "/auth"
|
||||
|
||||
def authorize(
|
||||
self,
|
||||
user_id: str,
|
||||
provider: AuthProvider | str,
|
||||
provider_type: AuthProviderType = AuthProviderType.oauth2,
|
||||
scopes: list[str] | None = None,
|
||||
) -> AuthResponse:
|
||||
"""
|
||||
Initiate an authorization request.
|
||||
|
||||
Args:
|
||||
provider: The authorization provider.
|
||||
scopes: The scopes required for the authorization.
|
||||
user_id: The user ID initiating the authorization.
|
||||
"""
|
||||
auth_provider_type = provider_type.value
|
||||
|
||||
body = {
|
||||
"auth_requirement": {
|
||||
"provider_id": provider.value if isinstance(provider, AuthProvider) else provider,
|
||||
"provider_type": auth_provider_type,
|
||||
auth_provider_type: AuthRequest(scopes=scopes or []).model_dump(exclude_none=True),
|
||||
},
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"POST",
|
||||
f"{self._resource_path}/authorize",
|
||||
json=body,
|
||||
)
|
||||
return AuthResponse(**data)
|
||||
|
||||
def status(
|
||||
self,
|
||||
auth_id_or_response: Union[str, AuthResponse],
|
||||
scopes: list[str] | None = None,
|
||||
wait: int | None = None,
|
||||
) -> AuthResponse:
|
||||
"""
|
||||
Poll for the status of an authorization
|
||||
|
||||
Polls using either the authorization ID or the data returned from the authorize method.
|
||||
|
||||
Example:
|
||||
auth_response = client.auth.authorize(...)
|
||||
auth_status = client.auth.poll_authorization(auth_response)
|
||||
auth_status = client.auth.poll_authorization("auth_123", ["scope1", "scope2"])
|
||||
"""
|
||||
if isinstance(auth_id_or_response, AuthResponse):
|
||||
auth_id = auth_id_or_response.auth_id
|
||||
scopes = auth_id_or_response.scopes
|
||||
else:
|
||||
auth_id = auth_id_or_response
|
||||
|
||||
# Calculate the new timeout based on the wait parameter
|
||||
new_timeout = self._client._timeout
|
||||
if wait is not None:
|
||||
if isinstance(self._client._timeout, Timeout):
|
||||
new_timeout = Timeout(
|
||||
connect=self._client._timeout.connect,
|
||||
read=(self._client._timeout.read or 0) + wait,
|
||||
write=self._client._timeout.write,
|
||||
pool=self._client._timeout.pool,
|
||||
)
|
||||
else:
|
||||
new_timeout = self._client._timeout + wait
|
||||
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}/status",
|
||||
params={
|
||||
"authorizationId": auth_id,
|
||||
"scopes": " ".join(scopes) if scopes else None,
|
||||
"wait": wait,
|
||||
},
|
||||
timeout=new_timeout,
|
||||
)
|
||||
return AuthResponse(**data)
|
||||
|
||||
|
||||
class ToolResource(BaseResource[ClientT]):
|
||||
"""Tool resource."""
|
||||
|
||||
_path = "/tools"
|
||||
|
||||
def run(
|
||||
self,
|
||||
tool_name: str,
|
||||
user_id: str,
|
||||
tool_version: str | None = None,
|
||||
inputs: dict[str, Any] | str | None = None,
|
||||
) -> ExecuteToolResponse:
|
||||
"""
|
||||
Send a request to execute a tool and return the response.
|
||||
|
||||
Args:
|
||||
tool_name: The name of the tool to execute.
|
||||
user_id: The user ID initiating the tool execution.
|
||||
tool_version: The version of the tool to execute (if not provided, the latest version will be used).
|
||||
inputs: The inputs for the tool.
|
||||
"""
|
||||
if not isinstance(inputs, str):
|
||||
try:
|
||||
inputs = json.dumps(inputs)
|
||||
except Exception:
|
||||
raise ValueError("Inputs must be a valid JSON object or serializable dictionary")
|
||||
|
||||
request_data = {
|
||||
"tool_name": tool_name,
|
||||
"user_id": user_id,
|
||||
"tool_version": tool_version,
|
||||
"inputs": inputs,
|
||||
}
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"POST", f"{self._resource_path}/execute", json=request_data
|
||||
)
|
||||
return ExecuteToolResponse(**data)
|
||||
|
||||
def get(self, director_id: str, tool_id: str) -> ToolDefinition:
|
||||
"""
|
||||
Get the specification for a tool.
|
||||
"""
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}/definition",
|
||||
params={"directorId": director_id, "toolId": tool_id},
|
||||
)
|
||||
return ToolDefinition(**data)
|
||||
|
||||
def authorize(
|
||||
self, tool_name: str, user_id: str, tool_version: str | None = None
|
||||
) -> AuthResponse:
|
||||
"""
|
||||
Get the authorization status for a tool.
|
||||
"""
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"POST",
|
||||
f"{self._resource_path}/authorize",
|
||||
json={"tool_name": tool_name, "tool_version": tool_version, "user_id": user_id},
|
||||
)
|
||||
return AuthResponse(**data)
|
||||
|
||||
def list_tools(self, toolkit: str | None = None) -> list[ToolDefinition]:
|
||||
"""
|
||||
List the tools available for a given toolkit and provider.
|
||||
"""
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}/list",
|
||||
params={"toolkit": toolkit},
|
||||
)
|
||||
return [ToolDefinition(**tool) for tool in data]
|
||||
|
||||
|
||||
class HealthResource(BaseResource[ClientT]):
|
||||
"""Health check resource."""
|
||||
|
||||
_path = "/health"
|
||||
|
||||
def check(self) -> None:
|
||||
"""
|
||||
Check the health of the Arcade Engine.
|
||||
Raises an error if the health check fails.
|
||||
"""
|
||||
|
||||
try:
|
||||
data = self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}",
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
except APIStatusError as e:
|
||||
raise EngineNotHealthyError(
|
||||
"Arcade Engine health check returned an unhealthy status code",
|
||||
status_code=e.status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
# Catches everything else including httpx.ConnectError (most common)
|
||||
raise EngineOfflineError(f"Arcade Engine was unreachable: {e}")
|
||||
|
||||
health_check_response = HealthCheckResponse(**data)
|
||||
|
||||
# Raise an error if the health payload is not `healthy: true`
|
||||
if health_check_response.healthy is not True:
|
||||
raise EngineNotHealthyError(
|
||||
"Arcade Engine health check was not healthy",
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
class AsyncAuthResource(BaseResource[AsyncArcadeClient]):
|
||||
"""Asynchronous Authentication resource."""
|
||||
|
||||
_path = "/auth"
|
||||
|
||||
async def authorize(
|
||||
self,
|
||||
user_id: str,
|
||||
provider: AuthProvider | str,
|
||||
provider_type: AuthProviderType = AuthProviderType.oauth2,
|
||||
scopes: list[str] | None = None,
|
||||
) -> AuthResponse:
|
||||
"""
|
||||
Initiate an asynchronous authorization request.
|
||||
"""
|
||||
auth_provider_type = provider_type.value
|
||||
|
||||
body = {
|
||||
"auth_requirement": {
|
||||
"provider_id": provider.value if isinstance(provider, AuthProvider) else provider,
|
||||
"provider_type": auth_provider_type,
|
||||
auth_provider_type: AuthRequest(scopes=scopes or []).model_dump(exclude_none=True),
|
||||
},
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"POST",
|
||||
f"{self._resource_path}/authorize",
|
||||
json=body,
|
||||
)
|
||||
return AuthResponse(**data)
|
||||
|
||||
async def status(
|
||||
self,
|
||||
auth_id_or_response: Union[str, AuthResponse],
|
||||
scopes: list[str] | None = None,
|
||||
wait: int | None = None,
|
||||
) -> AuthResponse:
|
||||
"""
|
||||
Poll for the status of an authorization asynchronously
|
||||
|
||||
Polls using either the authorization ID or the data returned from the authorize method.
|
||||
|
||||
Example:
|
||||
auth_response = await client.auth.authorize(...)
|
||||
auth_status = await client.auth.poll_authorization(auth_response)
|
||||
auth_status = await client.auth.poll_authorization("auth_123", ["scope1", "scope2"])
|
||||
"""
|
||||
if isinstance(auth_id_or_response, AuthResponse):
|
||||
auth_id = auth_id_or_response.auth_id
|
||||
scopes = auth_id_or_response.scopes
|
||||
else:
|
||||
auth_id = auth_id_or_response
|
||||
|
||||
# Calculate the new timeout based on the wait parameter
|
||||
new_timeout = self._client._timeout
|
||||
if wait is not None:
|
||||
if isinstance(self._client._timeout, Timeout):
|
||||
new_timeout = Timeout(
|
||||
connect=self._client._timeout.connect,
|
||||
read=(self._client._timeout.read or 0) + wait,
|
||||
write=self._client._timeout.write,
|
||||
pool=self._client._timeout.pool,
|
||||
)
|
||||
else:
|
||||
new_timeout = self._client._timeout + wait
|
||||
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}/status",
|
||||
params={"authorizationId": auth_id, "scopes": " ".join(scopes) if scopes else None},
|
||||
timeout=new_timeout,
|
||||
)
|
||||
return AuthResponse(**data)
|
||||
|
||||
|
||||
class AsyncToolResource(BaseResource[AsyncArcadeClient]):
|
||||
"""Asynchronous Tool resource."""
|
||||
|
||||
_path = "/tools"
|
||||
|
||||
async def run(
|
||||
self,
|
||||
tool_name: str,
|
||||
user_id: str,
|
||||
tool_version: str | None = None,
|
||||
inputs: dict[str, Any] | None = None,
|
||||
) -> ExecuteToolResponse:
|
||||
"""
|
||||
Send an asynchronous request to execute a tool and return the response.
|
||||
"""
|
||||
request_data = {
|
||||
"tool_name": tool_name,
|
||||
"user_id": user_id,
|
||||
"tool_version": tool_version,
|
||||
"inputs": inputs,
|
||||
}
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"POST", f"{self._resource_path}/execute", json=request_data
|
||||
)
|
||||
return ExecuteToolResponse(**data)
|
||||
|
||||
async def get(self, director_id: str, tool_id: str) -> ToolDefinition:
|
||||
"""
|
||||
Get the specification for a tool asynchronously.
|
||||
"""
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}/definition",
|
||||
params={"directorId": director_id, "toolId": tool_id},
|
||||
)
|
||||
return ToolDefinition(**data)
|
||||
|
||||
async def authorize(
|
||||
self, tool_name: str, user_id: str, tool_version: str | None = None
|
||||
) -> AuthResponse:
|
||||
"""
|
||||
Get the authorization status for a tool.
|
||||
"""
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"POST",
|
||||
f"{self._resource_path}/authorize",
|
||||
json={"tool_name": tool_name, "tool_version": tool_version, "user_id": user_id},
|
||||
)
|
||||
return AuthResponse(**data)
|
||||
|
||||
async def list_tools(self, toolkit: str | None = None) -> list[ToolDefinition]:
|
||||
"""
|
||||
List the tools available for a given toolkit and provider.
|
||||
"""
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}/list",
|
||||
params={"toolkit": toolkit},
|
||||
)
|
||||
return [ToolDefinition(**tool) for tool in data]
|
||||
|
||||
|
||||
class AsyncHealthResource(BaseResource[AsyncArcadeClient]):
|
||||
"""Asynchronous Health check resource."""
|
||||
|
||||
_path = "/health"
|
||||
|
||||
async def check(self) -> None:
|
||||
"""
|
||||
Check the health of the Arcade Engine.
|
||||
Raises an error if the health check fails.
|
||||
"""
|
||||
|
||||
try:
|
||||
data = await self._client._execute_request( # type: ignore[attr-defined]
|
||||
"GET",
|
||||
f"{self._resource_path}",
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
except APIStatusError as e:
|
||||
raise EngineNotHealthyError(
|
||||
"Arcade Engine health check returned an unhealthy status code",
|
||||
status_code=e.status_code,
|
||||
)
|
||||
except Exception as e:
|
||||
# Catches everything else including httpx.ConnectError (most common)
|
||||
raise EngineOfflineError(f"Arcade Engine was unreachable: {e}")
|
||||
|
||||
health_check_response = HealthCheckResponse(**data)
|
||||
|
||||
# Raise an error if the health payload is not `healthy: true`
|
||||
if health_check_response.healthy is not True:
|
||||
raise EngineNotHealthyError(
|
||||
"Arcade Engine health check was not healthy",
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
class Arcade(SyncArcadeClient):
|
||||
"""Synchronous Arcade client."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.auth: AuthResource = AuthResource(self)
|
||||
self.tools: ToolResource = ToolResource(self)
|
||||
self.health: HealthResource = HealthResource(self)
|
||||
|
||||
def _execute_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Execute a synchronous request.
|
||||
"""
|
||||
response = self._request(method, url, **kwargs)
|
||||
return response.json()
|
||||
|
||||
|
||||
class AsyncArcade(AsyncArcadeClient):
|
||||
"""Asynchronous Arcade client."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.auth: AsyncAuthResource = AsyncAuthResource(self)
|
||||
self.tools: AsyncToolResource = AsyncToolResource(self)
|
||||
self.health: AsyncHealthResource = AsyncHealthResource(self)
|
||||
|
||||
async def _execute_request(self, method: str, url: str, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Execute an asynchronous request.
|
||||
"""
|
||||
response = await self._request(method, url, **kwargs)
|
||||
return response.json()
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class ArcadeError(Exception):
|
||||
"""Top-level exception for Arcade Client errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EngineOfflineError(ArcadeError):
|
||||
"""Raised when the Arcade Engine is offline."""
|
||||
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class EngineNotHealthyError(ArcadeError):
|
||||
"""Raised when the Arcade Engine is not healthy."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int,
|
||||
):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class APIError(ArcadeError):
|
||||
"""Base class for API-related errors."""
|
||||
|
||||
def __init__(self, message: str, request: httpx.Request, *, body: Optional[object] = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.request = request
|
||||
self.body = body
|
||||
|
||||
|
||||
class APIStatusError(APIError):
|
||||
"""Raised when an API response has a status code of 4xx or 5xx."""
|
||||
|
||||
def __init__(self, message: str, *, response: httpx.Response, body: Optional[object] = None):
|
||||
super().__init__(message, response.request, body=body)
|
||||
self.response = response
|
||||
self.status_code = response.status_code
|
||||
|
||||
|
||||
class BadRequestError(APIStatusError):
|
||||
"""400 Bad Request"""
|
||||
|
||||
status_code = 400
|
||||
|
||||
|
||||
class UnauthorizedError(APIStatusError):
|
||||
"""401 Unauthorized"""
|
||||
|
||||
status_code = 401
|
||||
|
||||
|
||||
class PermissionDeniedError(APIStatusError):
|
||||
"""403 Forbidden"""
|
||||
|
||||
status_code = 403
|
||||
|
||||
|
||||
class NotFoundError(APIStatusError):
|
||||
"""404 Not Found"""
|
||||
|
||||
status_code = 404
|
||||
|
||||
|
||||
class APITimeoutError(APIStatusError):
|
||||
"""408 Request Timeout"""
|
||||
|
||||
status_code = 408
|
||||
|
||||
|
||||
class RateLimitError(APIStatusError):
|
||||
"""429 Too Many Requests"""
|
||||
|
||||
status_code = 429
|
||||
|
||||
|
||||
class InternalServerError(APIStatusError):
|
||||
"""500 Internal Server Error"""
|
||||
|
||||
status_code = 500
|
||||
|
||||
|
||||
class UnprocessableEntityError(APIStatusError):
|
||||
"""422 Unprocessable Entity"""
|
||||
|
||||
status_code = 422
|
||||
|
||||
|
||||
class ServiceUnavailableError(APIStatusError):
|
||||
"""503 Service Unavailable"""
|
||||
|
||||
status_code = 503
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from arcade.core.schema import ToolAuthorizationContext, ToolCallOutput
|
||||
|
||||
|
||||
class AuthProvider(str, Enum):
|
||||
google = "google"
|
||||
"""Google authorization"""
|
||||
|
||||
slack = "slack_user"
|
||||
"""Slack (user token) authorization"""
|
||||
|
||||
github = "github"
|
||||
"""GitHub authorization"""
|
||||
|
||||
|
||||
class AuthProviderType(str, Enum):
|
||||
"""The supported authorization provider types."""
|
||||
|
||||
oauth2 = "oauth2"
|
||||
"""OAuth 2.0 authorization"""
|
||||
|
||||
|
||||
class AuthRequest(BaseModel):
|
||||
"""
|
||||
The requirements for authorization for a tool
|
||||
# TODO (Nate): Make a validator here
|
||||
"""
|
||||
|
||||
scopes: list[str]
|
||||
"""The scope(s) needed for authorization."""
|
||||
|
||||
|
||||
class AuthStatus(str, Enum):
|
||||
"""The status of an authorization request."""
|
||||
|
||||
pending = "pending"
|
||||
failed = "failed"
|
||||
completed = "completed"
|
||||
|
||||
|
||||
class HealthCheckResponse(BaseModel):
|
||||
"""Response from a health check request."""
|
||||
|
||||
healthy: bool
|
||||
"""Whether the health check was successful."""
|
||||
|
||||
|
||||
class AuthResponse(BaseModel):
|
||||
"""Response from an authorization request."""
|
||||
|
||||
auth_id: str = Field(alias="authorization_id")
|
||||
"""The ID of the authorization request"""
|
||||
|
||||
scopes: list[str]
|
||||
"""The scope(s) requested in the authorization request"""
|
||||
|
||||
# TODO: Use AnyUrl?
|
||||
auth_url: str | None = Field(None, alias="authorization_url")
|
||||
"""The URL for the authorization"""
|
||||
|
||||
status: AuthStatus
|
||||
"""Only completed implies presence of a token"""
|
||||
|
||||
context: ToolAuthorizationContext | None = None
|
||||
|
||||
|
||||
class ExecuteToolResponse(BaseModel):
|
||||
"""Response from executing a tool."""
|
||||
|
||||
invocation_id: str
|
||||
"""The globally-unique ID for this tool invocation in the run."""
|
||||
|
||||
duration: float
|
||||
"""The duration of the tool invocation in milliseconds."""
|
||||
|
||||
finished_at: str
|
||||
"""The timestamp when the tool invocation finished."""
|
||||
|
||||
success: bool
|
||||
"""Whether the tool invocation was successful."""
|
||||
|
||||
output: ToolCallOutput | None = None
|
||||
"""The output of the tool invocation."""
|
||||
|
|
@ -129,8 +129,6 @@ class Config(BaseConfig):
|
|||
6. Hostnames with underscores (common in development environments) are supported.
|
||||
7. Pre-existing port specifications in the host are respected.
|
||||
|
||||
The resulting URL is always suffixed with '/v1' to specify the API version.
|
||||
|
||||
Returns:
|
||||
str: The fully constructed URL for the Arcade Engine.
|
||||
|
||||
|
|
@ -175,14 +173,14 @@ class Config(BaseConfig):
|
|||
if ":" in parsed_host.netloc and not is_ip:
|
||||
host, existing_port = parsed_host.netloc.rsplit(":", 1)
|
||||
if existing_port.isdigit():
|
||||
return f"{protocol}://{parsed_host.netloc}/{self.api.version}"
|
||||
return f"{protocol}://{parsed_host.netloc}"
|
||||
|
||||
if is_fqdn and self.engine.port is None:
|
||||
return f"{protocol}://{encoded_host}/{self.api.version}"
|
||||
return f"{protocol}://{encoded_host}"
|
||||
elif self.engine.port is not None:
|
||||
return f"{protocol}://{encoded_host}:{self.engine.port}/{self.api.version}"
|
||||
return f"{protocol}://{encoded_host}:{self.engine.port}"
|
||||
else:
|
||||
return f"{protocol}://{encoded_host}/{self.api.version}"
|
||||
return f"{protocol}://{encoded_host}"
|
||||
|
||||
@classmethod
|
||||
def ensure_config_dir_exists(cls) -> None:
|
||||
|
|
|
|||
|
|
@ -1,152 +0,0 @@
|
|||
import dataclasses
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
class CustomCodeBase(Enum):
|
||||
"""Custom status code base class"""
|
||||
|
||||
@property
|
||||
def code(self) -> Any:
|
||||
"""
|
||||
Get status code
|
||||
"""
|
||||
return self.value[0]
|
||||
|
||||
@property
|
||||
def msg(self) -> Any:
|
||||
"""
|
||||
Get status code information
|
||||
"""
|
||||
return self.value[1]
|
||||
|
||||
|
||||
class CustomResponseCode(CustomCodeBase):
|
||||
"""Custom response status codes"""
|
||||
|
||||
HTTP_200 = (200, "Request Successful")
|
||||
HTTP_201 = (201, "Created Successfully")
|
||||
HTTP_202 = (202, "Request Accepted, but Processing Not Yet Complete")
|
||||
HTTP_204 = (204, "Request Successful, but No Content Returned")
|
||||
HTTP_400 = (400, "Bad Request")
|
||||
HTTP_401 = (401, "Unauthorized")
|
||||
HTTP_403 = (403, "Forbidden Access")
|
||||
HTTP_404 = (404, "Requested Resource Not Found")
|
||||
HTTP_410 = (410, "Requested Resource Permanently Deleted")
|
||||
HTTP_422 = (422, "Invalid Request Parameters")
|
||||
HTTP_425 = (425, "Request Unexecutable, as Server Cannot Meet Requirements")
|
||||
HTTP_429 = (429, "Too Many Requests, Server Limiting")
|
||||
HTTP_500 = (500, "Internal Server Error")
|
||||
HTTP_502 = (502, "Gateway Error")
|
||||
HTTP_503 = (503, "Server Temporarily Unable to Process Request")
|
||||
HTTP_504 = (504, "Gateway Timeout")
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CustomResponse:
|
||||
"""
|
||||
Provides open response status codes, rather than enums, which can be useful if you want to customize response information
|
||||
"""
|
||||
|
||||
code: int
|
||||
msg: str
|
||||
|
||||
|
||||
class StandardResponseCode:
|
||||
"""Standard response status codes"""
|
||||
|
||||
"""
|
||||
HTTP codes
|
||||
See HTTP Status Code Registry:
|
||||
https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
|
||||
And RFC 2324 - https://tools.ietf.org/html/rfc2324
|
||||
"""
|
||||
HTTP_100 = 100 # CONTINUE
|
||||
HTTP_101 = 101 # SWITCHING_PROTOCOLS
|
||||
HTTP_102 = 102 # PROCESSING
|
||||
HTTP_103 = 103 # EARLY_HINTS
|
||||
HTTP_200 = 200 # OK
|
||||
HTTP_201 = 201 # CREATED
|
||||
HTTP_202 = 202 # ACCEPTED
|
||||
HTTP_203 = 203 # NON_AUTHORITATIVE_INFORMATION
|
||||
HTTP_204 = 204 # NO_CONTENT
|
||||
HTTP_205 = 205 # RESET_CONTENT
|
||||
HTTP_206 = 206 # PARTIAL_CONTENT
|
||||
HTTP_207 = 207 # MULTI_STATUS
|
||||
HTTP_208 = 208 # ALREADY_REPORTED
|
||||
HTTP_226 = 226 # IM_USED
|
||||
HTTP_300 = 300 # MULTIPLE_CHOICES
|
||||
HTTP_301 = 301 # MOVED_PERMANENTLY
|
||||
HTTP_302 = 302 # FOUND
|
||||
HTTP_303 = 303 # SEE_OTHER
|
||||
HTTP_304 = 304 # NOT_MODIFIED
|
||||
HTTP_305 = 305 # USE_PROXY
|
||||
HTTP_307 = 307 # TEMPORARY_REDIRECT
|
||||
HTTP_308 = 308 # PERMANENT_REDIRECT
|
||||
HTTP_400 = 400 # BAD_REQUEST
|
||||
HTTP_401 = 401 # UNAUTHORIZED
|
||||
HTTP_402 = 402 # PAYMENT_REQUIRED
|
||||
HTTP_403 = 403 # FORBIDDEN
|
||||
HTTP_404 = 404 # NOT_FOUND
|
||||
HTTP_405 = 405 # METHOD_NOT_ALLOWED
|
||||
HTTP_406 = 406 # NOT_ACCEPTABLE
|
||||
HTTP_407 = 407 # PROXY_AUTHENTICATION_REQUIRED
|
||||
HTTP_408 = 408 # REQUEST_TIMEOUT
|
||||
HTTP_409 = 409 # CONFLICT
|
||||
HTTP_410 = 410 # GONE
|
||||
HTTP_411 = 411 # LENGTH_REQUIRED
|
||||
HTTP_412 = 412 # PRECONDITION_FAILED
|
||||
HTTP_413 = 413 # REQUEST_ENTITY_TOO_LARGE
|
||||
HTTP_414 = 414 # REQUEST_URI_TOO_LONG
|
||||
HTTP_415 = 415 # UNSUPPORTED_MEDIA_TYPE
|
||||
HTTP_416 = 416 # REQUESTED_RANGE_NOT_SATISFIABLE
|
||||
HTTP_417 = 417 # EXPECTATION_FAILED
|
||||
HTTP_418 = 418 # UNUSED
|
||||
HTTP_421 = 421 # MISDIRECTED_REQUEST
|
||||
HTTP_422 = 422 # UNPROCESSABLE_CONTENT
|
||||
HTTP_423 = 423 # LOCKED
|
||||
HTTP_424 = 424 # FAILED_DEPENDENCY
|
||||
HTTP_425 = 425 # TOO_EARLY
|
||||
HTTP_426 = 426 # UPGRADE_REQUIRED
|
||||
HTTP_427 = 427 # UNASSIGNED
|
||||
HTTP_428 = 428 # PRECONDITION_REQUIRED
|
||||
HTTP_429 = 429 # TOO_MANY_REQUESTS
|
||||
HTTP_430 = 430 # Unassigned
|
||||
HTTP_431 = 431 # REQUEST_HEADER_FIELDS_TOO_LARGE
|
||||
HTTP_451 = 451 # UNAVAILABLE_FOR_LEGAL_REASONS
|
||||
HTTP_500 = 500 # INTERNAL_SERVER_ERROR
|
||||
HTTP_501 = 501 # NOT_IMPLEMENTED
|
||||
HTTP_502 = 502 # BAD_GATEWAY
|
||||
HTTP_503 = 503 # SERVICE_UNAVAILABLE
|
||||
HTTP_504 = 504 # GATEWAY_TIMEOUT
|
||||
HTTP_505 = 505 # HTTP_VERSION_NOT_SUPPORTED
|
||||
HTTP_506 = 506 # VARIANT_ALSO_NEGOTIATES
|
||||
HTTP_507 = 507 # INSUFFICIENT_STORAGE
|
||||
HTTP_508 = 508 # LOOP_DETECTED
|
||||
HTTP_509 = 509 # UNASSIGNED
|
||||
HTTP_510 = 510 # NOT_EXTENDED
|
||||
HTTP_511 = 511 # NETWORK_AUTHENTICATION_REQUIRED
|
||||
|
||||
"""
|
||||
WebSocket codes
|
||||
https://www.iana.org/assignments/websocket/websocket.xml#close-code-number
|
||||
https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
||||
"""
|
||||
WS_1000 = 1000 # NORMAL_CLOSURE
|
||||
WS_1001 = 1001 # GOING_AWAY
|
||||
WS_1002 = 1002 # PROTOCOL_ERROR
|
||||
WS_1003 = 1003 # UNSUPPORTED_DATA
|
||||
WS_1005 = 1005 # NO_STATUS_RCVD
|
||||
WS_1006 = 1006 # ABNORMAL_CLOSURE
|
||||
WS_1007 = 1007 # INVALID_FRAME_PAYLOAD_DATA
|
||||
WS_1008 = 1008 # POLICY_VIOLATION
|
||||
WS_1009 = 1009 # MESSAGE_TOO_BIG
|
||||
WS_1010 = 1010 # MANDATORY_EXT
|
||||
WS_1011 = 1011 # INTERNAL_ERROR
|
||||
WS_1012 = 1012 # SERVICE_RESTART
|
||||
WS_1013 = 1013 # TRY_AGAIN_LATER
|
||||
WS_1014 = 1014 # BAD_GATEWAY
|
||||
WS_1015 = 1015 # TLS_HANDSHAKE
|
||||
WS_3000 = 3000 # UNAUTHORIZED
|
||||
WS_3003 = 3003 # FORBIDDEN
|
||||
|
|
@ -654,7 +654,7 @@ def tool_eval() -> Callable[[Callable], Callable]:
|
|||
results = []
|
||||
async with AsyncOpenAI(
|
||||
api_key=config.api.key,
|
||||
base_url=config.engine_url,
|
||||
base_url=config.engine_url + "/v1", # TODO remove
|
||||
) as client:
|
||||
result = await suite.run(client, model)
|
||||
results.append(result)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ rich = "^13.7.1"
|
|||
toml = "^0.10.2"
|
||||
tomlkit = "^0.12.4"
|
||||
openai = "^1.36.0" # TODO: relax to an earlier version that still has what we need
|
||||
arcadepy = "~0.1.0"
|
||||
pyjwt = "^2.8.0"
|
||||
loguru = "^0.7.0"
|
||||
types-python-dateutil = "2.9.0.20241003"
|
||||
|
|
|
|||
|
|
@ -1,396 +0,0 @@
|
|||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
from httpx import HTTPStatusError, Response
|
||||
|
||||
from arcade.client import Arcade, AsyncArcade, AuthProvider
|
||||
from arcade.client.errors import (
|
||||
APITimeoutError,
|
||||
BadRequestError,
|
||||
EngineNotHealthyError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
PermissionDeniedError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from arcade.client.schema import AuthProviderType, AuthResponse, ExecuteToolResponse
|
||||
from arcade.core.schema import ToolDefinition
|
||||
|
||||
AUTH_RESPONSE_DATA = {
|
||||
"auth_id": "auth_123",
|
||||
"authorization_url": "https://example.com/auth",
|
||||
"status": "pending",
|
||||
"authorization_id": "auth_123",
|
||||
"scopes": ["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
}
|
||||
|
||||
AUTH_RESPONSE_DATA_NO_SCOPES = {
|
||||
"auth_id": "auth_123",
|
||||
"authorization_url": "https://example.com/auth",
|
||||
"status": "pending",
|
||||
"authorization_id": "auth_123",
|
||||
"scopes": [],
|
||||
}
|
||||
|
||||
TOOL_RESPONSE_DATA = {
|
||||
"tool_name": "GetEmails",
|
||||
"tool_version": "0.1.0",
|
||||
"output": {"result": "Hello, World!"},
|
||||
"error": None,
|
||||
"invocation_id": "inv_123",
|
||||
"duration": 1.5,
|
||||
"finished_at": "2023-04-01T12:00:00Z",
|
||||
"success": True,
|
||||
}
|
||||
|
||||
TOOL_DEFINITION_DATA = {
|
||||
"name": "GetEmails",
|
||||
"fully_qualified_name": "TestToolkit.GetEmails",
|
||||
"description": "Retrieve emails from a user's inbox",
|
||||
"toolkit": {
|
||||
"name": "TestToolkit",
|
||||
"version": "0.1.0",
|
||||
"description": "A toolkit for testing",
|
||||
},
|
||||
"input_schema": {"type": "object", "properties": {"n_emails": {"type": "integer"}}},
|
||||
"output_schema": {"type": "array", "items": {"type": "string"}},
|
||||
"version": "0.1.0",
|
||||
"inputs": {"parameters": []},
|
||||
"output": {},
|
||||
"requirements": {"auth_requirements": []},
|
||||
}
|
||||
|
||||
TOOL_AUTHORIZE_RESPONSE_DATA = {
|
||||
"authorization_id": "auth_456",
|
||||
"authorization_url": "https://example.com/auth",
|
||||
"scopes": ["scope1", "scope2"],
|
||||
"status": "pending",
|
||||
}
|
||||
|
||||
HEALTH_CHECK_HEALTHY_RESPONSE_DATA = {
|
||||
"healthy": True,
|
||||
}
|
||||
|
||||
HEALTH_CHECK_UNHEALTHY_RESPONSE_DATA = {
|
||||
"healthy": False,
|
||||
"reason": "Cannot reticulate splines",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_sync_client():
|
||||
"""Test client."""
|
||||
return Arcade(base_url="http://arcade.example.com", api_key="fake_api_key")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_async_client():
|
||||
"""Test client."""
|
||||
return AsyncArcade(base_url="http://arcade.example.com", api_key="fake_api_key")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response():
|
||||
"""Mock Response object for testing."""
|
||||
response = Mock(spec=Response)
|
||||
response.json.return_value = {}
|
||||
return response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_async_response():
|
||||
"""Mock AsyncResponse object for testing."""
|
||||
response = AsyncMock(spec=Response)
|
||||
response.json.return_value = {}
|
||||
return response
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_code, expected_error",
|
||||
[
|
||||
(400, BadRequestError),
|
||||
(401, UnauthorizedError),
|
||||
(403, PermissionDeniedError),
|
||||
(404, NotFoundError),
|
||||
(408, APITimeoutError),
|
||||
(500, InternalServerError),
|
||||
],
|
||||
)
|
||||
def test_handle_http_error(test_sync_client, error_code, expected_error, mock_response):
|
||||
"""Test _handle_http_error method for different error codes."""
|
||||
mock_response.status_code = error_code
|
||||
mock_response.json.return_value = {"error": "Test error message"}
|
||||
|
||||
# Create a mock HTTPStatusError
|
||||
mock_http_error = Mock(spec=HTTPStatusError)
|
||||
mock_http_error.response = mock_response
|
||||
|
||||
with pytest.raises(expected_error):
|
||||
test_sync_client._handle_http_error(mock_http_error) # Call the method on the instance
|
||||
|
||||
|
||||
def test_arcade_auth_authorize(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.auth.authorize method."""
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: AUTH_RESPONSE_DATA)
|
||||
auth_response = test_sync_client.auth.authorize(
|
||||
provider=AuthProvider.google,
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
def test_arcade_auth_authorize_with_provider_type(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.auth.authorize method."""
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: AUTH_RESPONSE_DATA)
|
||||
auth_response = test_sync_client.auth.authorize(
|
||||
provider="hooli",
|
||||
provider_type=AuthProviderType.oauth2,
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
def test_arcade_auth_authorize_with_no_scopes(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.auth.authorize method."""
|
||||
monkeypatch.setattr(
|
||||
Arcade, "_execute_request", lambda *args, **kwargs: AUTH_RESPONSE_DATA_NO_SCOPES
|
||||
)
|
||||
auth_response = test_sync_client.auth.authorize(
|
||||
provider=AuthProvider.google,
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA_NO_SCOPES)
|
||||
|
||||
|
||||
def test_arcade_auth_poll_authorization(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.auth.poll_authorization method."""
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: AUTH_RESPONSE_DATA)
|
||||
auth_response = test_sync_client.auth.status("auth_123")
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
def test_arcade_auth_long_poll_authorization(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.auth.poll_authorization method with long polling."""
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: AUTH_RESPONSE_DATA)
|
||||
auth_response = test_sync_client.auth.status("auth_123", wait=1)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
def test_arcade_tool_run(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.tools.run method."""
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: TOOL_RESPONSE_DATA)
|
||||
tool_response = test_sync_client.tools.run(
|
||||
tool_name="GetEmails",
|
||||
user_id="sam@arcade-ai.com",
|
||||
tool_version="0.1.0",
|
||||
inputs={"n_emails": 5},
|
||||
)
|
||||
assert tool_response == ExecuteToolResponse(**TOOL_RESPONSE_DATA)
|
||||
|
||||
|
||||
def test_arcade_tool_get(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.tools.get method."""
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: TOOL_DEFINITION_DATA)
|
||||
tool_definition = test_sync_client.tools.get(director_id="default", tool_id="GetEmails")
|
||||
assert tool_definition == ToolDefinition(**TOOL_DEFINITION_DATA)
|
||||
|
||||
|
||||
def test_arcade_tool_authorize(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.tools.authorize method."""
|
||||
monkeypatch.setattr(
|
||||
Arcade, "_execute_request", lambda *args, **kwargs: TOOL_AUTHORIZE_RESPONSE_DATA
|
||||
)
|
||||
auth_response = test_sync_client.tools.authorize(
|
||||
tool_name="GetEmails", user_id="sam@arcade-ai.com"
|
||||
)
|
||||
assert auth_response == AuthResponse(**TOOL_AUTHORIZE_RESPONSE_DATA)
|
||||
|
||||
|
||||
def test_arcade_health_check(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.health.check method."""
|
||||
monkeypatch.setattr(
|
||||
Arcade, "_execute_request", lambda *args, **kwargs: HEALTH_CHECK_HEALTHY_RESPONSE_DATA
|
||||
)
|
||||
test_sync_client.health.check()
|
||||
assert True # If no exception is raised, the test passes
|
||||
|
||||
|
||||
def test_arcade_health_check_raises_error(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.health.check method."""
|
||||
monkeypatch.setattr(
|
||||
Arcade, "_execute_request", lambda *args, **kwargs: HEALTH_CHECK_UNHEALTHY_RESPONSE_DATA
|
||||
)
|
||||
with pytest.raises(EngineNotHealthyError):
|
||||
test_sync_client.health.check()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_auth_authorize(test_async_client, mock_async_response, monkeypatch):
|
||||
"""Test AsyncArcade.auth.authorize method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return AUTH_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
auth_response = await test_async_client.auth.authorize(
|
||||
provider=AuthProvider.google,
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_auth_authorize_with_provider_type(
|
||||
test_async_client, mock_async_response, monkeypatch
|
||||
):
|
||||
"""Test AsyncArcade.auth.authorize method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return AUTH_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
auth_response = await test_async_client.auth.authorize(
|
||||
provider="hooli",
|
||||
provider_type=AuthProviderType.oauth2,
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_auth_authorize_with_no_scopes(
|
||||
test_async_client, mock_async_response, monkeypatch
|
||||
):
|
||||
"""Test AsyncArcade.auth.authorize method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return AUTH_RESPONSE_DATA_NO_SCOPES
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
auth_response = await test_async_client.auth.authorize(
|
||||
provider=AuthProvider.google,
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA_NO_SCOPES)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_auth_poll_authorization(
|
||||
test_async_client, mock_async_response, monkeypatch
|
||||
):
|
||||
"""Test AsyncArcade.auth.poll_authorization method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return AUTH_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
auth_response = await test_async_client.auth.status("auth_123")
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_auth_long_poll_authorization(
|
||||
test_async_client, mock_async_response, monkeypatch
|
||||
):
|
||||
"""Test AsyncArcade.auth.poll_authorization method with long polling."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return AUTH_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
auth_response = await test_async_client.auth.status("auth_123", wait=1)
|
||||
assert auth_response == AuthResponse(**AUTH_RESPONSE_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_tool_run(test_async_client, mock_async_response, monkeypatch):
|
||||
"""Test AsyncArcade.tools.run method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return TOOL_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
tool_response = await test_async_client.tools.run(
|
||||
tool_name="GetEmails",
|
||||
user_id="sam@arcade-ai.com",
|
||||
tool_version="0.1.0",
|
||||
inputs={"n_emails": 5},
|
||||
)
|
||||
assert tool_response == ExecuteToolResponse(**TOOL_RESPONSE_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_tool_get(test_async_client, mock_async_response, monkeypatch):
|
||||
"""Test AsyncArcade.tools.get method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return TOOL_DEFINITION_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
tool_definition = await test_async_client.tools.get(director_id="default", tool_id="GetEmails")
|
||||
assert tool_definition == ToolDefinition(**TOOL_DEFINITION_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_tool_authorize(test_async_client, mock_async_response, monkeypatch):
|
||||
"""Test AsyncArcade.tools.authorize method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return TOOL_AUTHORIZE_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
auth_response = await test_async_client.tools.authorize(
|
||||
tool_name="GetEmails", user_id="sam@arcade-ai.com"
|
||||
)
|
||||
assert auth_response == AuthResponse(**TOOL_AUTHORIZE_RESPONSE_DATA)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_health_check(test_async_client, mock_async_response, monkeypatch):
|
||||
"""Test AsyncArcade.health.check method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return HEALTH_CHECK_HEALTHY_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
await test_async_client.health.check()
|
||||
assert True # If no exception is raised, the test passes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_health_check_raises_error(
|
||||
test_async_client, mock_async_response, monkeypatch
|
||||
):
|
||||
"""Test AsyncArcade.health.check method."""
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return HEALTH_CHECK_UNHEALTHY_RESPONSE_DATA
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
with pytest.raises(EngineNotHealthyError):
|
||||
await test_async_client.health.check()
|
||||
|
||||
|
||||
def test_arcade_tool_list_tools(test_sync_client, mock_response, monkeypatch):
|
||||
"""Test Arcade.tools.list_tools method."""
|
||||
data = [TOOL_DEFINITION_DATA]
|
||||
monkeypatch.setattr(Arcade, "_execute_request", lambda *args, **kwargs: data)
|
||||
tool_definitions = test_sync_client.tools.list_tools(toolkit="TestToolkit")
|
||||
assert tool_definitions == [ToolDefinition(**TOOL_DEFINITION_DATA)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_arcade_tool_list_tools(test_async_client, mock_async_response, monkeypatch):
|
||||
"""Test AsyncArcade.tools.list_tools method."""
|
||||
data = [TOOL_DEFINITION_DATA]
|
||||
|
||||
async def mock_execute_request(*args, **kwargs):
|
||||
return data
|
||||
|
||||
monkeypatch.setattr(AsyncArcade, "_execute_request", mock_execute_request)
|
||||
tool_definitions = await test_async_client.tools.list_tools(toolkit="TestToolkit")
|
||||
assert tool_definitions == [ToolDefinition(**TOOL_DEFINITION_DATA)]
|
||||
|
|
@ -4,6 +4,7 @@ ignorePaths:
|
|||
dictionaryDefinitions: []
|
||||
dictionaries: []
|
||||
words:
|
||||
- arcadepy
|
||||
- conlist
|
||||
- fastapi
|
||||
- httpx
|
||||
|
|
|
|||
26
examples/bareclient/get_gmail_emails.py
Normal file
26
examples/bareclient/get_gmail_emails.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from arcadepy import Arcade
|
||||
|
||||
client = Arcade(
|
||||
base_url="http://localhost:9099",
|
||||
)
|
||||
|
||||
user_id = "you@example.com"
|
||||
|
||||
# Start the authorization process
|
||||
auth_response = client.tools.authorize(
|
||||
tool_name="Google.ListEmails",
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
if auth_response.status != "completed":
|
||||
print(f"Click this link to authorize: {auth_response.authorization_url}")
|
||||
input("After you have authorized, press Enter to continue...")
|
||||
|
||||
inputs = {"n_emails": 5}
|
||||
|
||||
response = client.tools.execute(
|
||||
tool_name="Google.ListEmails",
|
||||
inputs=inputs,
|
||||
user_id=user_id,
|
||||
)
|
||||
print(response)
|
||||
34
examples/bareclient/get_gmail_token.py
Normal file
34
examples/bareclient/get_gmail_token.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from arcadepy import Arcade
|
||||
from arcadepy.types.auth_authorize_params import AuthRequirement, AuthRequirementOauth2
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
client = Arcade(
|
||||
base_url="http://localhost:9099",
|
||||
)
|
||||
|
||||
user_id = "you@example.com"
|
||||
|
||||
# Start the authorization process
|
||||
auth_response = client.auth.authorize(
|
||||
auth_requirement=AuthRequirement(
|
||||
provider_id="google",
|
||||
oauth2=AuthRequirementOauth2(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
),
|
||||
),
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
if auth_response.status != "completed":
|
||||
print(f"Click this link to authorize: {auth_response.authorization_url}")
|
||||
input("After you have authorized, press Enter to continue...")
|
||||
|
||||
# Use the token from the authorization response
|
||||
creds = Credentials(auth_response.context.token)
|
||||
service = build("gmail", "v1", credentials=creds)
|
||||
|
||||
# Now you can use the Google API
|
||||
results = service.users().labels().list(userId="me").execute()
|
||||
labels = results.get("labels", [])
|
||||
print("Labels:", labels)
|
||||
|
|
@ -2,17 +2,17 @@ import os
|
|||
|
||||
import arcade_math
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from arcade.actor.fastapi.actor import FastAPIActor
|
||||
from arcade.client import AsyncArcade
|
||||
from arcade.core.config import config
|
||||
from arcade.core.toolkit import Toolkit
|
||||
|
||||
if not config.api or not config.api.key:
|
||||
raise ValueError("Arcade API key not set. Please run `arcade login`.")
|
||||
|
||||
client = AsyncArcade(api_key=config.api.key)
|
||||
client = AsyncOpenAI(api_key=config.api.key, base_url="http://localhost:9099/v1")
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import time # Import time for polling delays
|
||||
from typing import cast
|
||||
|
||||
from arcadepy import NOT_GIVEN, Arcade
|
||||
from arcadepy.types.auth_authorize_params import AuthRequirement, AuthRequirementOauth2
|
||||
from google.oauth2.credentials import Credentials
|
||||
from langchain_google_community import GmailToolkit
|
||||
from langchain_google_community.gmail.utils import (
|
||||
|
|
@ -13,28 +15,33 @@ from langgraph.prebuilt import create_react_agent
|
|||
# %pip install -qU langchain-google-community[gmail]
|
||||
# %pip install -qU langchain-openai
|
||||
# %pip install -qU langgraph
|
||||
from arcade.client import Arcade, AuthProvider
|
||||
|
||||
client = Arcade()
|
||||
|
||||
# Start the authorization process for the tool "ListEmails"
|
||||
auth_response = client.auth.authorize(
|
||||
provider=AuthProvider.google,
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
auth_requirement=AuthRequirement(
|
||||
provider_id="google",
|
||||
oauth2=AuthRequirementOauth2(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
),
|
||||
),
|
||||
user_id="sam@arcade-ai.com",
|
||||
)
|
||||
|
||||
# If authorization is not completed, prompt the user and poll for status
|
||||
if auth_response.status != "completed":
|
||||
print("Please complete the authorization challenge in your browser before continuing:")
|
||||
print(auth_response.auth_url)
|
||||
print(auth_response.authorization_url)
|
||||
input("Press Enter to continue...")
|
||||
|
||||
# Poll for authorization status using the auth polling method
|
||||
while auth_response.status != "completed":
|
||||
# Wait before polling again to avoid spamming the server
|
||||
time.sleep(4)
|
||||
auth_response = client.auth.status(auth_response)
|
||||
auth_response = client.auth.status(
|
||||
authorization_id=cast(str, auth_response.authorization_id),
|
||||
scopes=" ".join(auth_response.scopes) if auth_response.scopes else NOT_GIVEN,
|
||||
wait=30, # Long poll
|
||||
)
|
||||
|
||||
# Authorization is completed; proceed with obtaining credentials
|
||||
creds = Credentials(auth_response.context.token)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from arcadepy import Arcade
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.errors import NodeInterrupt
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
from arcade.client import Arcade
|
||||
|
||||
client = Arcade(api_key=os.environ["ARCADE_API_KEY"])
|
||||
|
||||
|
||||
|
|
@ -26,11 +24,10 @@ def step_1(state: State, config) -> State:
|
|||
if challenge.status != "completed":
|
||||
raise NodeInterrupt(f"Please visit this URL to authorize: {challenge.auth_url}")
|
||||
|
||||
result = client.tools.run(
|
||||
result = client.tools.execute(
|
||||
tool_name="ListEmails",
|
||||
user_id=user_id,
|
||||
tool_version="default",
|
||||
inputs=json.dumps({"n_emails": 5}),
|
||||
inputs={"n_emails": 5},
|
||||
)
|
||||
return {"emails": result}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue