Source code for camel.toolkits.aci_toolkit

# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========

import os
from typing import TYPE_CHECKING, Dict, List, Optional, Union

if TYPE_CHECKING:
    from aci.types.app_configurations import AppConfiguration
    from aci.types.apps import AppBasic, AppDetails
    from aci.types.linked_accounts import LinkedAccount

from camel.logger import get_logger
from camel.toolkits import FunctionTool
from camel.toolkits.base import BaseToolkit
from camel.utils import api_keys_required, dependencies_required

logger = get_logger(__name__)


[docs] @api_keys_required( [ (None, 'ACI_API_KEY'), ] ) class ACIToolkit(BaseToolkit): r"""A toolkit for interacting with the ACI API.""" @dependencies_required('aci') def __init__( self, api_key: Optional[str] = None, base_url: Optional[str] = None, linked_account_owner_id: Optional[str] = None, timeout: Optional[float] = None, ) -> None: r"""Initialize the ACI toolkit. Args: api_key (Optional[str]): The API key for authentication. (default: :obj: `None`) base_url (Optional[str]): The base URL for the ACI API. (default: :obj: `None`) linked_account_owner_id (Optional[str]): ID of the owner of the linked account, e.g., "johndoe" (default: :obj: `None`) timeout (Optional[float]): Request timeout. (default: :obj: `None`) """ from aci import ACI super().__init__(timeout) self._api_key = api_key or os.getenv("ACI_API_KEY") self._base_url = base_url or os.getenv("ACI_BASE_URL") self.client = ACI(api_key=self._api_key, base_url=self._base_url) self.linked_account_owner_id = linked_account_owner_id def search_tool( self, intent: Optional[str] = None, allowed_app_only: bool = True, include_functions: bool = False, categories: Optional[List[str]] = None, limit: Optional[int] = 10, offset: Optional[int] = 0, ) -> Union[List["AppBasic"], str]: r"""Search for apps based on intent. Args: intent (Optional[str]): Search results will be sorted by relevance to this intent. (default: :obj: `None`) allowed_app_only (bool): If true, only return apps that are allowed by the agent/accessor, identified by the api key. (default: :obj: `True`) include_functions (bool): If true, include functions (name and description) in the search results. (default: :obj: `False`) categories (Optional[List[str]]): List of categories to filter the search results. Defaults to an empty list. (default: :obj: `None`) limit (Optional[int]): Maximum number of results to return. (default: :obj: `10`) offset (Optional[int]): Offset for pagination. (default: :obj: `0`) Returns: Optional[List[AppBasic]]: List of matching apps if successful, error message otherwise. """ try: apps = self.client.apps.search( intent=intent, allowed_apps_only=allowed_app_only, include_functions=include_functions, categories=categories, limit=limit, offset=offset, ) return apps except Exception as e: logger.error(f"Error: {e}") return str(e) def list_configured_apps( self, app_names: Optional[List[str]] = None, limit: Optional[int] = 10, offset: Optional[int] = 0, ) -> Union[List["AppConfiguration"], str]: r"""List all configured apps. Args: app_names (Optional[List[str]]): List of app names to filter the results. (default: :obj: `None`) limit (Optional[int]): Maximum number of results to return. (default: :obj: `10`) offset (Optional[int]): Offset for pagination. (default: :obj: `0`) Returns: Union[List[AppConfiguration], str]: List of configured apps if successful, error message otherwise. """ try: apps = self.client.app_configurations.list( app_names=app_names, limit=limit, offset=offset ) return apps except Exception as e: logger.error(f"Error: {e}") return str(e) def configure_app(self, app_name: str) -> Union[Dict, str]: r"""Configure an app with specified authentication type. Args: app_name (str): Name of the app to configure. Returns: Union[Dict, str]: Configuration result or error message. """ from aci.types.enums import SecurityScheme try: app_details = self.get_app_details(app_name) if app_details and app_details.security_schemes[0] == "api_key": security_scheme = SecurityScheme.API_KEY elif app_details and app_details.security_schemes[0] == "oauth2": security_scheme = SecurityScheme.OAUTH2 else: security_scheme = SecurityScheme.NO_AUTH configuration = self.client.app_configurations.create( app_name=app_name, security_scheme=security_scheme ) return configuration except Exception as e: logger.error(f"Error: {e}") return str(e) def get_app_configuration( self, app_name: str ) -> Union["AppConfiguration", str]: r"""Get app configuration by app name. Args: app_name (str): Name of the app to get configuration for. Returns: Union[AppConfiguration, str]: App configuration if successful, error message otherwise. """ try: app = self.client.app_configurations.get(app_name=app_name) return app except Exception as e: logger.error(f"Error: {e}") return str(e) def delete_app(self, app_name: str) -> Optional[str]: r"""Delete an app configuration. Args: app_name (str): Name of the app to delete. Returns: Optional[str]: None if successful, error message otherwise. """ try: self.client.app_configurations.delete(app_name=app_name) return None except Exception as e: logger.error(f"Error: {e}") return str(e) def link_account( self, app_name: str, ) -> Union["LinkedAccount", str]: r"""Link an account to a configured app. Args: app_name (str): Name of the app to link the account to. Returns: Union[LinkedAccount, str]: LinkedAccount object if successful, error message otherwise. """ from aci.types.enums import SecurityScheme try: security_scheme = self.client.app_configurations.get( app_name=app_name ).security_scheme if security_scheme == SecurityScheme.API_KEY: return self.client.linked_accounts.link( app_name=app_name, linked_account_owner_id=self.linked_account_owner_id, security_scheme=security_scheme, api_key=self._api_key, ) else: return self.client.linked_accounts.link( app_name=app_name, linked_account_owner_id=self.linked_account_owner_id, security_scheme=security_scheme, ) except Exception as e: logger.error(f"Error linking account: {e!s}") return str(e) def get_app_details(self, app_name: str) -> "AppDetails": r"""Get details of an app. Args: app_name (str): Name of the app to get details for. Returns: AppDetails: App details. """ app = self.client.apps.get(app_name=app_name) return app def get_linked_accounts( self, app_name: str ) -> Union[List["LinkedAccount"], str]: r"""List all linked accounts for a specific app. Args: app_name (str): Name of the app to get linked accounts for. Returns: Union[List[LinkedAccount], str]: List of linked accounts if successful, error message otherwise. """ try: accounts = self.client.linked_accounts.list(app_name=app_name) return accounts except Exception as e: logger.error(f"Error: {e}") return str(e) def enable_linked_account( self, linked_account_id: str ) -> Union["LinkedAccount", str]: r"""Enable a linked account. Args: linked_account_id (str): ID of the linked account to enable. Returns: Union[LinkedAccount, str]: Linked account if successful, error message otherwise. """ try: linked_account = self.client.linked_accounts.enable( linked_account_id=linked_account_id ) return linked_account except Exception as e: logger.error(f"Error: {e}") return str(e) def disable_linked_account( self, linked_account_id: str ) -> Union["LinkedAccount", str]: r"""Disable a linked account. Args: linked_account_id (str): ID of the linked account to disable. Returns: Union[LinkedAccount, str]: The updated linked account if successful, error message otherwise. """ try: linked_account = self.client.linked_accounts.disable( linked_account_id=linked_account_id ) return linked_account except Exception as e: logger.error(f"Error: {e}") return str(e) def delete_linked_account(self, linked_account_id: str) -> str: r"""Delete a linked account. Args: linked_account_id (str): ID of the linked account to delete. Returns: str: Success message if successful, error message otherwise. """ try: self.client.linked_accounts.delete( linked_account_id=linked_account_id ) return ( f"linked_account_id: {linked_account_id} deleted successfully" ) except Exception as e: logger.error(f"Error: {e}") return str(e) def function_definition(self, func_name: str) -> Dict: r"""Get the function definition for an app. Args: app_name (str): Name of the app to get function definition for Returns: Dict: Function definition dictionary. """ return self.client.functions.get_definition(func_name) def search_function( self, app_names: Optional[List[str]] = None, intent: Optional[str] = None, allowed_apps_only: bool = True, limit: Optional[int] = 10, offset: Optional[int] = 0, ) -> List[Dict]: r"""Search for functions based on intent. Args: app_names (Optional[List[str]]): List of app names to filter the search results. (default: :obj: `None`) intent (Optional[str]): The search query/intent. (default: :obj: `None`) allowed_apps_only (bool): If true, only return functions from allowed apps. (default: :obj: `True`) limit (Optional[int]): Maximum number of results to return. (default: :obj: `10`) offset (Optional[int]): Offset for pagination. (default: :obj: `0`) Returns: List[Dict]: List of matching functions """ return self.client.functions.search( app_names=app_names, intent=intent, allowed_apps_only=allowed_apps_only, limit=limit, offset=offset, ) def execute_function( self, function_name: str, function_arguments: Dict, linked_account_owner_id: str, allowed_apps_only: bool = False, ) -> Dict: r"""Execute a function call. Args: function_name (str): Name of the function to execute. function_arguments (Dict): Arguments to pass to the function. linked_account_owner_id (str): To specify the end-user (account owner) on behalf of whom you want to execute functions You need to first link corresponding account with the same owner id in the ACI dashboard (https://platform.aci.dev). allowed_apps_only (bool): If true, only returns functions/apps that are allowed to be used by the agent/accessor, identified by the api key. (default: :obj: `False`) Returns: Dict: Result of the function execution """ result = self.client.handle_function_call( function_name, function_arguments, linked_account_owner_id, allowed_apps_only, ) return result def get_tools(self) -> List[FunctionTool]: r"""Get a list of tools (functions) available in the configured apps. Returns: List[FunctionTool]: List of FunctionTool objects representing available functions """ _configure_app = [ app.app_name # type: ignore[union-attr] for app in self.list_configured_apps() or [] ] _all_function = self.search_function(app_names=_configure_app) tools = [ FunctionTool(self.search_tool), FunctionTool(self.list_configured_apps), FunctionTool(self.configure_app), FunctionTool(self.get_app_configuration), FunctionTool(self.delete_app), FunctionTool(self.link_account), FunctionTool(self.get_app_details), FunctionTool(self.get_linked_accounts), FunctionTool(self.enable_linked_account), FunctionTool(self.disable_linked_account), FunctionTool(self.delete_linked_account), FunctionTool(self.function_definition), FunctionTool(self.search_function), ] for function in _all_function: schema = self.client.functions.get_definition( function['function']['name'] ) def dummy_func(*, schema=schema, **kwargs): return self.execute_function( function_name=schema['function']['name'], function_arguments=kwargs, linked_account_owner_id=self.linked_account_owner_id, ) tool = FunctionTool( func=dummy_func, openai_tool_schema=schema, ) tools.append(tool) return tools