arcade-mcp/toolkits/gmail/arcade_gmail/tools/gmail.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

189 lines
5.4 KiB
Python

import base64
import json
import re
from base64 import urlsafe_b64decode
from email.mime.text import MIMEText
from typing import Annotated, Any, Dict, Optional
from bs4 import BeautifulSoup
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from arcade.core.schema import ToolContext
from arcade.sdk import tool
from arcade.sdk.auth import Google
@tool(
requires_auth=Google(
scope=["https://www.googleapis.com/auth/gmail.compose"],
)
)
async def write_draft(
context: ToolContext,
subject: Annotated[str, "The subject of the email"],
body: Annotated[str, "The body of the email"],
recipient: Annotated[str, "The recipient of the email"],
) -> Annotated[str, "The URL of the draft"]:
"""Compose a new email draft."""
# Set up the Gmail API client
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
message = MIMEText(body)
message["to"] = recipient
message["subject"] = subject
# Encode the message in base64
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
# Create the draft
draft = {"message": {"raw": raw_message}}
draft_message = service.users().drafts().create(userId="me", body=draft).execute()
return f"Draft created: {get_draft_url(draft_message['id'])}"
def get_draft_url(draft_id):
return f"https://mail.google.com/mail/u/0/#drafts/{draft_id}"
@tool(
requires_auth=Google(
scope=["https://www.googleapis.com/auth/gmail.readonly"],
)
)
async def get_emails(
context: ToolContext,
n_emails: Annotated[int, "Number of emails to read"] = 5,
) -> Annotated[str, "A list of email details in JSON format"]:
"""
Read emails from a Gmail account and extract plain text content.
Args:
context (ToolContext): The context containing authorization information.
n_emails (int): Number of emails to read (default: 5).
Returns:
Dict[str, List[Dict[str, str]]]: A dictionary containing a list of email details.
"""
service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
try:
messages = (
service.users().messages().list(userId="me").execute().get("messages", [])
)
if not messages:
return {"emails": []}
emails = []
for msg in messages[:n_emails]:
try:
email_data = (
service.users().messages().get(userId="me", id=msg["id"]).execute()
)
email_details = parse_email(email_data)
if email_details:
emails.append(email_details)
except Exception as e:
print(f"Error reading email {msg['id']}: {e}")
return json.dumps({"emails": emails})
except Exception as e:
print(f"Error reading emails: {e}")
return "Error reading emails"
def parse_email(email_data: Dict[str, Any]) -> Optional[Dict[str, str]]:
"""
Parse email data and extract relevant information.
Args:
email_data (Dict[str, Any]): Raw email data from Gmail API.
Returns:
Optional[Dict[str, str]]: Parsed email details or None if parsing fails.
"""
try:
payload = email_data["payload"]
headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
body_data = get_email_body(payload)
return {
"from": headers.get("from", ""),
"date": headers.get("date", ""),
"subject": headers.get("subject", "No subject"),
"body": clean_email_body(body_data) if body_data else "",
}
except Exception as e:
print(f"Error parsing email {email_data.get('id', 'unknown')}: {e}")
return None
def get_email_body(payload: Dict[str, Any]) -> Optional[str]:
"""
Extract email body from payload.
Args:
payload (Dict[str, Any]): Email payload data.
Returns:
Optional[str]: Decoded email body or None if not found.
"""
if "body" in payload and payload["body"].get("data"):
return urlsafe_b64decode(payload["body"]["data"]).decode()
for part in payload.get("parts", []):
if part.get("mimeType") == "text/plain" and "data" in part["body"]:
return urlsafe_b64decode(part["body"]["data"]).decode()
return None
def clean_email_body(body: str) -> str:
"""
Remove HTML tags and clean up email body text while preserving most content.
Args:
body (str): The raw email body text.
Returns:
str: Cleaned email body text.
"""
try:
# Remove HTML tags using BeautifulSoup
soup = BeautifulSoup(body, "html.parser")
text = soup.get_text(separator=" ")
# Clean up the text
text = clean_text(text)
return text.strip()
except Exception as e:
print(f"Error cleaning email body: {e}")
return body
def clean_text(text: str) -> str:
"""
Clean up the text while preserving most content.
Args:
text (str): The input text.
Returns:
str: Cleaned text.
"""
# Replace multiple newlines with a single newline
text = re.sub(r"\n+", "\n", text)
# Replace multiple spaces with a single space
text = re.sub(r"\s+", " ", text)
# Remove leading/trailing whitespace from each line
text = "\n".join(line.strip() for line in text.split("\n"))
return text