Separate tools & helper funcs in separate files (#192)
Separates utility and helper functions, as well as constant values (e.g. base URLs), in dedicated files, apart from tools files.
This commit is contained in:
parent
cd1fb648bd
commit
cd837a363d
17 changed files with 179 additions and 164 deletions
|
|
@ -8,7 +8,6 @@ from arcade.sdk.auth import Google
|
|||
from arcade.sdk.errors import RetryableToolError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.tools.utils import (
|
||||
DateRange,
|
||||
|
|
@ -19,6 +18,7 @@ from arcade_google.tools.utils import (
|
|||
get_sent_email_url,
|
||||
parse_draft_email,
|
||||
parse_email,
|
||||
process_email_messages,
|
||||
remove_none_values,
|
||||
)
|
||||
|
||||
|
|
@ -356,22 +356,10 @@ async def list_emails_by_header(
|
|||
if not messages:
|
||||
return {"emails": []}
|
||||
|
||||
emails = process_messages(service, messages)
|
||||
emails = process_email_messages(service, messages)
|
||||
return {"emails": emails}
|
||||
|
||||
|
||||
def process_messages(service: Any, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
emails = []
|
||||
for msg in messages:
|
||||
try:
|
||||
email_data = service.users().messages().get(userId="me", id=msg["id"]).execute()
|
||||
email_details = parse_email(email_data)
|
||||
emails += [email_details] if email_details else []
|
||||
except HttpError as e:
|
||||
print(f"Error reading email {msg['id']}: {e}")
|
||||
return emails
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Google(
|
||||
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from zoneinfo import ZoneInfo
|
|||
from bs4 import BeautifulSoup
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import Resource, build
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from arcade_google.tools.models import Day, TimeSlot
|
||||
|
||||
|
|
@ -72,6 +73,18 @@ class DateRange(Enum):
|
|||
return result + comparison_date.strftime("%Y/%m/%d")
|
||||
|
||||
|
||||
def process_email_messages(service: Any, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
emails = []
|
||||
for msg in messages:
|
||||
try:
|
||||
email_data = service.users().messages().get(userId="me", id=msg["id"]).execute()
|
||||
email_details = parse_email(email_data)
|
||||
emails += [email_details] if email_details else []
|
||||
except HttpError as e:
|
||||
print(f"Error reading email {msg['id']}: {e}")
|
||||
return emails
|
||||
|
||||
|
||||
def parse_email(email_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Parse email data and extract relevant information.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_google"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
description = "Arcade tools for the entire google suite"
|
||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
||||
|
||||
|
|
|
|||
1
toolkits/linkedin/arcade_linkedin/tools/constants.py
Normal file
1
toolkits/linkedin/arcade_linkedin/tools/constants.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
LINKEDIN_BASE_URL = "https://api.linkedin.com/v2"
|
||||
|
|
@ -1,74 +1,10 @@
|
|||
from typing import Annotated
|
||||
|
||||
import httpx
|
||||
from arcade.sdk import ToolContext, tool
|
||||
from arcade.sdk.auth import LinkedIn
|
||||
from arcade.sdk.errors import ToolExecutionError
|
||||
|
||||
LINKEDIN_BASE_URL = "https://api.linkedin.com/v2"
|
||||
|
||||
|
||||
async def _send_linkedin_request(
|
||||
context: ToolContext,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
) -> httpx.Response:
|
||||
"""
|
||||
Send an asynchronous request to the LinkedIn API.
|
||||
|
||||
Args:
|
||||
context: The tool context containing the authorization token.
|
||||
method: The HTTP method (GET, POST, PUT, DELETE, etc.).
|
||||
endpoint: The API endpoint path (e.g., "/ugcPosts").
|
||||
params: Query parameters to include in the request.
|
||||
json_data: JSON data to include in the request body.
|
||||
|
||||
Returns:
|
||||
The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the request fails for any reason.
|
||||
"""
|
||||
url = f"{LINKEDIN_BASE_URL}{endpoint}"
|
||||
token = (
|
||||
context.authorization.token if context.authorization and context.authorization.token else ""
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method, url, headers=headers, params=params, json=json_data
|
||||
)
|
||||
response.raise_for_status()
|
||||
except httpx.RequestError as e:
|
||||
raise ToolExecutionError(f"Failed to send request to LinkedIn API: {e}")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _handle_linkedin_api_error(response: httpx.Response) -> None:
|
||||
"""
|
||||
Handle errors from the LinkedIn API by mapping common status codes to ToolExecutionErrors.
|
||||
|
||||
Args:
|
||||
response: The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the response contains an error status code.
|
||||
"""
|
||||
status_code_map = {
|
||||
401: ToolExecutionError("Unauthorized: Invalid or expired token"),
|
||||
403: ToolExecutionError("Forbidden: User does not have Spotify Premium"),
|
||||
429: ToolExecutionError("Too Many Requests: Rate limit exceeded"),
|
||||
}
|
||||
|
||||
if response.status_code in status_code_map:
|
||||
raise status_code_map[response.status_code]
|
||||
elif response.status_code >= 400:
|
||||
raise ToolExecutionError(f"Error: {response.status_code} - {response.text}")
|
||||
from arcade_linkedin.tools.utils import _handle_linkedin_api_error, _send_linkedin_request
|
||||
|
||||
|
||||
@tool(
|
||||
|
|
@ -90,6 +26,7 @@ async def create_text_post(
|
|||
# LinkedIn calls the user ID "sub" in their user_info data payload. See:
|
||||
# https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#api-request-to-retreive-member-details
|
||||
user_id = context.authorization.user_info.get("sub") if context.authorization else None
|
||||
|
||||
if not user_id:
|
||||
raise ToolExecutionError(
|
||||
"User ID not found.",
|
||||
|
|
@ -110,9 +47,11 @@ async def create_text_post(
|
|||
}
|
||||
|
||||
response = await _send_linkedin_request(context, "POST", endpoint, json_data=payload)
|
||||
|
||||
if response.status_code >= 200 and response.status_code < 300:
|
||||
share_id = response.json().get("id")
|
||||
return f"https://www.linkedin.com/feed/update/{share_id}/"
|
||||
|
||||
_handle_linkedin_api_error(response)
|
||||
|
||||
return ""
|
||||
|
|
|
|||
68
toolkits/linkedin/arcade_linkedin/tools/utils.py
Normal file
68
toolkits/linkedin/arcade_linkedin/tools/utils.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import httpx
|
||||
from arcade.sdk import ToolContext
|
||||
from arcade.sdk.errors import ToolExecutionError
|
||||
|
||||
from arcade_linkedin.tools.constants import LINKEDIN_BASE_URL
|
||||
|
||||
|
||||
async def _send_linkedin_request(
|
||||
context: ToolContext,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
) -> httpx.Response:
|
||||
"""
|
||||
Send an asynchronous request to the LinkedIn API.
|
||||
|
||||
Args:
|
||||
context: The tool context containing the authorization token.
|
||||
method: The HTTP method (GET, POST, PUT, DELETE, etc.).
|
||||
endpoint: The API endpoint path (e.g., "/ugcPosts").
|
||||
params: Query parameters to include in the request.
|
||||
json_data: JSON data to include in the request body.
|
||||
|
||||
Returns:
|
||||
The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the request fails for any reason.
|
||||
"""
|
||||
url = f"{LINKEDIN_BASE_URL}{endpoint}"
|
||||
token = (
|
||||
context.authorization.token if context.authorization and context.authorization.token else ""
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method, url, headers=headers, params=params, json=json_data
|
||||
)
|
||||
response.raise_for_status()
|
||||
except httpx.RequestError as e:
|
||||
raise ToolExecutionError(f"Failed to send request to LinkedIn API: {e}")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _handle_linkedin_api_error(response: httpx.Response) -> None:
|
||||
"""
|
||||
Handle errors from the LinkedIn API by mapping common status codes to ToolExecutionErrors.
|
||||
|
||||
Args:
|
||||
response: The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the response contains an error status code.
|
||||
"""
|
||||
status_code_map = {
|
||||
401: ToolExecutionError("Unauthorized: Invalid or expired token"),
|
||||
403: ToolExecutionError("Forbidden: User does not have Spotify Premium"),
|
||||
429: ToolExecutionError("Too Many Requests: Rate limit exceeded"),
|
||||
}
|
||||
|
||||
if response.status_code in status_code_map:
|
||||
raise status_code_map[response.status_code]
|
||||
elif response.status_code >= 400:
|
||||
raise ToolExecutionError(f"Error: {response.status_code} - {response.text}")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_linkedin"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
description = "Arcade tools for LinkedIn"
|
||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Annotated, Any, Optional
|
||||
from typing import Annotated
|
||||
|
||||
import serpapi
|
||||
from arcade.sdk import tool
|
||||
|
||||
from arcade_search.tools.utils import get_secret
|
||||
|
||||
|
||||
@tool
|
||||
async def search_google(
|
||||
|
|
@ -25,12 +26,3 @@ async def search_google(
|
|||
organic_results = results.get("organic_results", [])
|
||||
|
||||
return json.dumps(organic_results[:n_results])
|
||||
|
||||
|
||||
def get_secret(name: str, default: Optional[Any] = None) -> Any:
|
||||
secret = os.getenv(name)
|
||||
if secret is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValueError(f"Secret {name} is not set.")
|
||||
return secret
|
||||
|
|
|
|||
11
toolkits/search/arcade_search/tools/utils.py
Normal file
11
toolkits/search/arcade_search/tools/utils.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def get_secret(name: str, default: Optional[Any] = None) -> Any:
|
||||
secret = os.getenv(name)
|
||||
if secret is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValueError(f"Secret {name} is not set.")
|
||||
return secret
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_search"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
description = "Tools for searching the web"
|
||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
||||
|
||||
|
|
|
|||
1
toolkits/x/arcade_x/tools/constants.py
Normal file
1
toolkits/x/arcade_x/tools/constants.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
TWEETS_URL = "https://api.x.com/2/tweets"
|
||||
|
|
@ -5,6 +5,7 @@ from arcade.sdk import ToolContext, tool
|
|||
from arcade.sdk.auth import X
|
||||
from arcade.sdk.errors import RetryableToolError
|
||||
|
||||
from arcade_x.tools.constants import TWEETS_URL
|
||||
from arcade_x.tools.utils import (
|
||||
expand_attached_media,
|
||||
expand_long_tweet,
|
||||
|
|
@ -15,9 +16,6 @@ from arcade_x.tools.utils import (
|
|||
remove_none_values,
|
||||
)
|
||||
|
||||
TWEETS_URL = "https://api.x.com/2/tweets"
|
||||
|
||||
|
||||
# Manage Tweets Tools. See developer docs for additional available parameters:
|
||||
# https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_x"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
description = "LLM tools for interacting with X (Twitter)"
|
||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
||||
|
||||
|
|
|
|||
1
toolkits/zoom/arcade_zoom/tools/constants.py
Normal file
1
toolkits/zoom/arcade_zoom/tools/constants.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
ZOOM_BASE_URL = "https://api.zoom.us/v2"
|
||||
|
|
@ -1,74 +1,9 @@
|
|||
from typing import Annotated, Optional
|
||||
|
||||
import httpx
|
||||
from arcade.sdk import ToolContext, tool
|
||||
from arcade.sdk.auth import Zoom
|
||||
from arcade.sdk.errors import ToolExecutionError
|
||||
|
||||
ZOOM_BASE_URL = "https://api.zoom.us/v2"
|
||||
|
||||
|
||||
async def _send_zoom_request(
|
||||
context: ToolContext,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
) -> httpx.Response:
|
||||
"""
|
||||
Send an asynchronous request to the Zoom API.
|
||||
|
||||
Args:
|
||||
context: The tool context containing the authorization token.
|
||||
method: The HTTP method (GET, POST, PUT, DELETE, etc.).
|
||||
endpoint: The API endpoint path (e.g., "/users/me/upcoming_meetings").
|
||||
params: Query parameters to include in the request.
|
||||
json_data: JSON data to include in the request body.
|
||||
|
||||
Returns:
|
||||
The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the request fails for any reason.
|
||||
"""
|
||||
url = f"{ZOOM_BASE_URL}{endpoint}"
|
||||
token = (
|
||||
context.authorization.token if context.authorization and context.authorization.token else ""
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method, url, headers=headers, params=params, json=json_data
|
||||
)
|
||||
response.raise_for_status()
|
||||
except httpx.RequestError as e:
|
||||
raise ToolExecutionError(f"Failed to send request to Zoom API: {e}")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _handle_zoom_api_error(response: httpx.Response) -> None:
|
||||
"""
|
||||
Handle errors from the Zoom API by mapping common status codes to ToolExecutionErrors.
|
||||
|
||||
Args:
|
||||
response: The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the response contains an error status code.
|
||||
"""
|
||||
status_code_map = {
|
||||
401: ToolExecutionError("Unauthorized: Invalid or expired token"),
|
||||
403: ToolExecutionError("Forbidden: Access denied"),
|
||||
429: ToolExecutionError("Too Many Requests: Rate limit exceeded"),
|
||||
}
|
||||
|
||||
if response.status_code in status_code_map:
|
||||
raise status_code_map[response.status_code]
|
||||
elif response.status_code >= 400:
|
||||
raise ToolExecutionError(f"Error: {response.status_code} - {response.text}")
|
||||
from arcade_zoom.tools.utils import _handle_zoom_api_error, _send_zoom_request
|
||||
|
||||
|
||||
@tool(
|
||||
|
|
|
|||
68
toolkits/zoom/arcade_zoom/tools/utils.py
Normal file
68
toolkits/zoom/arcade_zoom/tools/utils.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import httpx
|
||||
from arcade.sdk import ToolContext
|
||||
from arcade.sdk.errors import ToolExecutionError
|
||||
|
||||
from arcade_zoom.tools.constants import ZOOM_BASE_URL
|
||||
|
||||
|
||||
async def _send_zoom_request(
|
||||
context: ToolContext,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: dict | None = None,
|
||||
json_data: dict | None = None,
|
||||
) -> httpx.Response:
|
||||
"""
|
||||
Send an asynchronous request to the Zoom API.
|
||||
|
||||
Args:
|
||||
context: The tool context containing the authorization token.
|
||||
method: The HTTP method (GET, POST, PUT, DELETE, etc.).
|
||||
endpoint: The API endpoint path (e.g., "/users/me/upcoming_meetings").
|
||||
params: Query parameters to include in the request.
|
||||
json_data: JSON data to include in the request body.
|
||||
|
||||
Returns:
|
||||
The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the request fails for any reason.
|
||||
"""
|
||||
url = f"{ZOOM_BASE_URL}{endpoint}"
|
||||
token = (
|
||||
context.authorization.token if context.authorization and context.authorization.token else ""
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.request(
|
||||
method, url, headers=headers, params=params, json=json_data
|
||||
)
|
||||
response.raise_for_status()
|
||||
except httpx.RequestError as e:
|
||||
raise ToolExecutionError(f"Failed to send request to Zoom API: {e}")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _handle_zoom_api_error(response: httpx.Response) -> None:
|
||||
"""
|
||||
Handle errors from the Zoom API by mapping common status codes to ToolExecutionErrors.
|
||||
|
||||
Args:
|
||||
response: The response object from the API request.
|
||||
|
||||
Raises:
|
||||
ToolExecutionError: If the response contains an error status code.
|
||||
"""
|
||||
status_code_map = {
|
||||
401: ToolExecutionError("Unauthorized: Invalid or expired token"),
|
||||
403: ToolExecutionError("Forbidden: Access denied"),
|
||||
429: ToolExecutionError("Too Many Requests: Rate limit exceeded"),
|
||||
}
|
||||
|
||||
if response.status_code in status_code_map:
|
||||
raise status_code_map[response.status_code]
|
||||
elif response.status_code >= 400:
|
||||
raise ToolExecutionError(f"Error: {response.status_code} - {response.text}")
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "arcade_zoom"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
description = "Arcade tools for Zoom"
|
||||
authors = ["Arcade AI <dev@arcade-ai.com>"]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue