Files
letta-server/letta/functions/async_composio_toolset.py
2025-05-15 00:34:04 -07:00

107 lines
4.5 KiB
Python

import json
from typing import Any
import aiohttp
from composio import ComposioToolSet as BaseComposioToolSet
from composio.exceptions import (
ApiKeyNotProvidedError,
ComposioSDKError,
ConnectedAccountNotFoundError,
EnumMetadataNotFound,
EnumStringNotFound,
)
class AsyncComposioToolSet(BaseComposioToolSet, runtime="letta", description_char_limit=1024):
"""
Async version of ComposioToolSet client for interacting with Composio API
Used to asynchronously hit the execute action endpoint
https://docs.composio.dev/api-reference/api-reference/v3/tools/post-api-v-3-tools-execute-action
"""
def __init__(self, api_key: str, entity_id: str, lock: bool = True):
"""
Initialize the AsyncComposioToolSet client
Args:
api_key (str): Your Composio API key
entity_id (str): Your Composio entity ID
lock (bool): Whether to use locking (default: True)
"""
super().__init__(api_key=api_key, entity_id=entity_id, lock=lock)
self.headers = {
"Content-Type": "application/json",
"X-API-Key": self._api_key,
}
async def execute_action(
self,
action: str,
params: dict[str, Any] = {},
) -> dict[str, Any]:
"""
Execute an action asynchronously using the Composio API
Args:
action (str): The name of the action to execute
params (dict[str, Any], optional): Parameters for the action
Returns:
dict[str, Any]: The API response
Raises:
ApiKeyNotProvidedError: if the API key is not provided
ComposioSDKError: if a general Composio SDK error occurs
ConnectedAccountNotFoundError: if the connected account is not found
EnumMetadataNotFound: if enum metadata is not found
EnumStringNotFound: if enum string is not found
aiohttp.ClientError: if a network-related error occurs
ValueError: if an error with the parameters or response occurs
"""
API_VERSION = "v3"
endpoint = f"{self._base_url}/{API_VERSION}/tools/execute/{action}"
json_payload = {
"entity_id": self.entity_id,
"arguments": params or {},
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(endpoint, headers=self.headers, json=json_payload) as response:
print(response, response.status, response.reason, response.content)
if response.status == 200:
return await response.json()
else:
error_text = await response.text()
try:
error_json = json.loads(error_text)
error_message = error_json.get("message", error_text)
error_code = error_json.get("code")
# Handle specific error codes from Composio API
if error_code == 10401 or "API_KEY_NOT_FOUND" in error_message:
raise ApiKeyNotProvidedError()
if "connected account not found" in error_message.lower():
raise ConnectedAccountNotFoundError(f"Connected account not found: {error_message}")
if "enum metadata not found" in error_message.lower():
raise EnumMetadataNotFound(f"Enum metadata not found: {error_message}")
if "enum string not found" in error_message.lower():
raise EnumStringNotFound(f"Enum string not found: {error_message}")
except json.JSONDecodeError:
error_message = error_text
# If no specific error was identified, raise a general error
raise ValueError(f"API request failed with status {response.status}: {error_message}")
except aiohttp.ClientError as e:
# Wrap network errors in ComposioSDKError
raise ComposioSDKError(f"Network error when calling Composio API: {str(e)}")
except ValueError:
# Re-raise ValueError (which could be our custom error message or a JSON parsing error)
raise
except Exception as e:
# Catch any other exceptions and wrap them in ComposioSDKError
raise ComposioSDKError(f"Unexpected error when calling Composio API: {str(e)}")