From 32c5be8b7360b905f6e4318b87fd46fa4301e94b Mon Sep 17 00:00:00 2001 From: Volodymyr Machula Date: Mon, 29 Jan 2024 21:45:03 +0100 Subject: [PATCH] community[minor]: Connery Tool and Toolkit (#14506) ## Summary This PR implements the "Connery Action Tool" and "Connery Toolkit". Using them, you can integrate Connery actions into your LangChain agents and chains. Connery is an open-source plugin infrastructure for AI. With Connery, you can easily create a custom plugin with a set of actions and seamlessly integrate them into your LangChain agents and chains. Connery will handle the rest: runtime, authorization, secret management, access management, audit logs, and other vital features. Additionally, Connery and our community offer a wide range of ready-to-use open-source plugins for your convenience. Learn more about Connery: - GitHub: https://github.com/connery-io/connery-platform - Documentation: https://docs.connery.io - Twitter: https://twitter.com/connery_io ## TODOs - [x] API wrapper - [x] Integration tests - [x] Connery Action Tool - [x] Docs - [x] Example - [x] Integration tests - [x] Connery Toolkit - [x] Docs - [x] Example - [x] Formatting (`make format`) - [x] Linting (`make lint`) - [x] Testing (`make test`) --- docs/docs/integrations/toolkits/connery.ipynb | 136 +++++++++++++++ docs/docs/integrations/tools/connery.ipynb | 165 ++++++++++++++++++ .../agent_toolkits/__init__.py | 2 + .../agent_toolkits/connery/__init__.py | 7 + .../agent_toolkits/connery/toolkit.py | 51 ++++++ .../langchain_community/tools/__init__.py | 9 + .../tools/connery/__init__.py | 8 + .../tools/connery/models.py | 32 ++++ .../tools/connery/service.py | 165 ++++++++++++++++++ .../langchain_community/tools/connery/tool.py | 163 +++++++++++++++++ .../tools/connery/test_service.py | 41 +++++ .../unit_tests/agent_toolkits/test_imports.py | 1 + .../tests/unit_tests/tools/test_imports.py | 1 + .../tests/unit_tests/tools/test_public_api.py | 1 + 14 files changed, 782 insertions(+) create mode 100644 docs/docs/integrations/toolkits/connery.ipynb create mode 100644 docs/docs/integrations/tools/connery.ipynb create mode 100644 libs/community/langchain_community/agent_toolkits/connery/__init__.py create mode 100644 libs/community/langchain_community/agent_toolkits/connery/toolkit.py create mode 100644 libs/community/langchain_community/tools/connery/__init__.py create mode 100644 libs/community/langchain_community/tools/connery/models.py create mode 100644 libs/community/langchain_community/tools/connery/service.py create mode 100644 libs/community/langchain_community/tools/connery/tool.py create mode 100644 libs/community/tests/integration_tests/tools/connery/test_service.py diff --git a/docs/docs/integrations/toolkits/connery.ipynb b/docs/docs/integrations/toolkits/connery.ipynb new file mode 100644 index 0000000000..184b934b63 --- /dev/null +++ b/docs/docs/integrations/toolkits/connery.ipynb @@ -0,0 +1,136 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Connery Toolkit\n", + "\n", + "Using this toolkit, you can integrate Connery Actions into your LangChain agent.\n", + "\n", + "If you want to use only one particular Connery Action in your agent,\n", + "check out the [Connery Action Tool](/docs/integrations/tools/connery) documentation.\n", + "\n", + "## What is Connery?\n", + "\n", + "Connery is an open-source plugin infrastructure for AI.\n", + "\n", + "With Connery, you can easily create a custom plugin with a set of actions and seamlessly integrate them into your LangChain agent.\n", + "Connery will take care of critical aspects such as runtime, authorization, secret management, access management, audit logs, and other vital features.\n", + "\n", + "Furthermore, Connery, supported by our community, provides a diverse collection of ready-to-use open-source plugins for added convenience.\n", + "\n", + "Learn more about Connery:\n", + "\n", + "- GitHub: https://github.com/connery-io/connery\n", + "- Documentation: https://docs.connery.io\n", + "\n", + "## Prerequisites\n", + "\n", + "To use Connery Actions in your LangChain agent, you need to do some preparation:\n", + "\n", + "1. Set up the Connery runner using the [Quickstart](https://docs.connery.io/docs/runner/quick-start/) guide.\n", + "2. Install all the plugins with the actions you want to use in your agent.\n", + "3. Set environment variables `CONNERY_RUNNER_URL` and `CONNERY_RUNNER_API_KEY` so the toolkit can communicate with the Connery Runner.\n", + "\n", + "## Example of using Connery Toolkit\n", + "\n", + "In the example below, we create an agent that uses two Connery Actions to summarize a public webpage and send the summary by email:\n", + "\n", + "1. **Summarize public webpage** action from the [Summarization](https://github.com/connery-io/summarization-plugin) plugin.\n", + "2. **Send email** action from the [Gmail](https://github.com/connery-io/gmail) plugin.\n", + "\n", + "You can see a LangSmith trace of this example [here](https://smith.langchain.com/public/4af5385a-afe9-46f6-8a53-57fe2d63c5bc/r)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `CA72DFB0AB4DF6C830B43E14B0782F70` with `{'publicWebpageUrl': 'http://www.paulgraham.com/vb.html'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m{'summary': 'The author reflects on the concept of life being short and how having children made them realize the true brevity of life. They discuss how time can be converted into discrete quantities and how limited certain experiences are. The author emphasizes the importance of prioritizing and eliminating unnecessary things in life, as well as actively pursuing meaningful experiences. They also discuss the negative impact of getting caught up in online arguments and the need to be aware of how time is being spent. The author suggests pruning unnecessary activities, not waiting to do things that matter, and savoring the time one has.'}\u001b[0m\u001b[32;1m\u001b[1;3m\n", + "Invoking: `CABC80BB79C15067CA983495324AE709` with `{'recipient': 'test@example.com', 'subject': 'Summary of the webpage', 'body': 'Here is a short summary of the webpage http://www.paulgraham.com/vb.html:\\n\\nThe author reflects on the concept of life being short and how having children made them realize the true brevity of life. They discuss how time can be converted into discrete quantities and how limited certain experiences are. The author emphasizes the importance of prioritizing and eliminating unnecessary things in life, as well as actively pursuing meaningful experiences. They also discuss the negative impact of getting caught up in online arguments and the need to be aware of how time is being spent. The author suggests pruning unnecessary activities, not waiting to do things that matter, and savoring the time one has.\\n\\nYou can find the full webpage [here](http://www.paulgraham.com/vb.html).'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[33;1m\u001b[1;3m{'messageId': '<2f04b00e-122d-c7de-c91e-e78e0c3276d6@gmail.com>'}\u001b[0m\u001b[32;1m\u001b[1;3mI have sent the email with the summary of the webpage to test@example.com. Please check your inbox.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "I have sent the email with the summary of the webpage to test@example.com. Please check your inbox.\n" + ] + } + ], + "source": [ + "import os\n", + "\n", + "from langchain.agents import AgentType, initialize_agent\n", + "from langchain.chat_models import ChatOpenAI\n", + "from langchain_community.agent_toolkits.connery import ConneryToolkit\n", + "from langchain_community.tools.connery import ConneryService\n", + "\n", + "# Specify your Connery Runner credentials.\n", + "os.environ[\"CONNERY_RUNNER_URL\"] = \"\"\n", + "os.environ[\"CONNERY_RUNNER_API_KEY\"] = \"\"\n", + "\n", + "# Specify OpenAI API key.\n", + "os.environ[\"OPENAI_API_KEY\"] = \"\"\n", + "\n", + "# Specify your email address to receive the email with the summary from example below.\n", + "recepient_email = \"test@example.com\"\n", + "\n", + "# Create a Connery Toolkit with all the available actions from the Connery Runner.\n", + "connery_service = ConneryService()\n", + "connery_toolkit = ConneryToolkit.create_instance(connery_service)\n", + "\n", + "# Use OpenAI Functions agent to execute the prompt using actions from the Connery Toolkit.\n", + "llm = ChatOpenAI(temperature=0)\n", + "agent = initialize_agent(\n", + " connery_toolkit.get_tools(), llm, AgentType.OPENAI_FUNCTIONS, verbose=True\n", + ")\n", + "result = agent.run(\n", + " f\"\"\"Make a short summary of the webpage http://www.paulgraham.com/vb.html in three sentences\n", + "and send it to {recepient_email}. Include the link to the webpage into the body of the email.\"\"\"\n", + ")\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "NOTE: Connery Action is a structured tool, so you can only use it in the agents supporting structured tools." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/docs/integrations/tools/connery.ipynb b/docs/docs/integrations/tools/connery.ipynb new file mode 100644 index 0000000000..a5c0829693 --- /dev/null +++ b/docs/docs/integrations/tools/connery.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Connery Action Tool\n", + "\n", + "Using this tool, you can integrate individual Connery Action into your LangChain agent.\n", + "\n", + "If you want to use more than one Connery Action in your agent,\n", + "check out the [Connery Toolkit](/docs/integrations/toolkits/connery) documentation.\n", + "\n", + "## What is Connery?\n", + "\n", + "Connery is an open-source plugin infrastructure for AI.\n", + "\n", + "With Connery, you can easily create a custom plugin with a set of actions and seamlessly integrate them into your LangChain agent.\n", + "Connery will take care of critical aspects such as runtime, authorization, secret management, access management, audit logs, and other vital features.\n", + "\n", + "Furthermore, Connery, supported by our community, provides a diverse collection of ready-to-use open-source plugins for added convenience.\n", + "\n", + "Learn more about Connery:\n", + "\n", + "- GitHub: https://github.com/connery-io/connery\n", + "- Documentation: https://docs.connery.io\n", + "\n", + "## Prerequisites\n", + "\n", + "To use Connery Actions in your LangChain agent, you need to do some preparation:\n", + "\n", + "1. Set up the Connery runner using the [Quickstart](https://docs.connery.io/docs/runner/quick-start/) guide.\n", + "2. Install all the plugins with the actions you want to use in your agent.\n", + "3. Set environment variables `CONNERY_RUNNER_URL` and `CONNERY_RUNNER_API_KEY` so the toolkit can communicate with the Connery Runner.\n", + "\n", + "## Example of using Connery Action Tool\n", + "\n", + "In the example below, we fetch action by its ID from the Connery Runner and then call it with the specified parameters.\n", + "\n", + "Here, we use the ID of the **Send email** action from the [Gmail](https://github.com/connery-io/gmail) plugin." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from langchain.agents import AgentType, initialize_agent\n", + "from langchain.chat_models import ChatOpenAI\n", + "from langchain_community.tools.connery import ConneryService\n", + "\n", + "# Specify your Connery Runner credentials.\n", + "os.environ[\"CONNERY_RUNNER_URL\"] = \"\"\n", + "os.environ[\"CONNERY_RUNNER_API_KEY\"] = \"\"\n", + "\n", + "# Specify OpenAI API key.\n", + "os.environ[\"OPENAI_API_KEY\"] = \"\"\n", + "\n", + "# Specify your email address to receive the emails from examples below.\n", + "recepient_email = \"test@example.com\"\n", + "\n", + "# Get the SendEmail action from the Connery Runner by ID.\n", + "connery_service = ConneryService()\n", + "send_email_action = connery_service.get_action(\"CABC80BB79C15067CA983495324AE709\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the action manually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "manual_run_result = send_email_action.run(\n", + " {\n", + " \"recipient\": recepient_email,\n", + " \"subject\": \"Test email\",\n", + " \"body\": \"This is a test email sent from Connery.\",\n", + " }\n", + ")\n", + "print(manual_run_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the action using the OpenAI Functions agent.\n", + "\n", + "You can see a LangSmith trace of this example [here](https://smith.langchain.com/public/a37d216f-c121-46da-a428-0e09dc19b1dc/r)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Invoking: `CABC80BB79C15067CA983495324AE709` with `{'recipient': 'test@example.com', 'subject': 'Late for Meeting', 'body': 'Dear Team,\\n\\nI wanted to inform you that I will be late for the meeting today. I apologize for any inconvenience caused. Please proceed with the meeting without me and I will join as soon as I can.\\n\\nBest regards,\\n[Your Name]'}`\n", + "\n", + "\n", + "\u001b[0m\u001b[36;1m\u001b[1;3m{'messageId': ''}\u001b[0m\u001b[32;1m\u001b[1;3mI have sent an email to test@example.com informing them that you will be late for the meeting.\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "I have sent an email to test@example.com informing them that you will be late for the meeting.\n" + ] + } + ], + "source": [ + "llm = ChatOpenAI(temperature=0)\n", + "agent = initialize_agent(\n", + " [send_email_action], llm, AgentType.OPENAI_FUNCTIONS, verbose=True\n", + ")\n", + "agent_run_result = agent.run(\n", + " f\"Send an email to the {recepient_email} and say that I will be late for the meeting.\"\n", + ")\n", + "print(agent_run_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "NOTE: Connery Action is a structured tool, so you can only use it in the agents supporting structured tools." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/libs/community/langchain_community/agent_toolkits/__init__.py b/libs/community/langchain_community/agent_toolkits/__init__.py index 9b5279ee85..3f6bf30331 100644 --- a/libs/community/langchain_community/agent_toolkits/__init__.py +++ b/libs/community/langchain_community/agent_toolkits/__init__.py @@ -18,6 +18,7 @@ from langchain_community.agent_toolkits.amadeus.toolkit import AmadeusToolkit from langchain_community.agent_toolkits.azure_cognitive_services import ( AzureCognitiveServicesToolkit, ) +from langchain_community.agent_toolkits.connery import ConneryToolkit from langchain_community.agent_toolkits.file_management.toolkit import ( FileManagementToolkit, ) @@ -50,6 +51,7 @@ __all__ = [ "AINetworkToolkit", "AmadeusToolkit", "AzureCognitiveServicesToolkit", + "ConneryToolkit", "FileManagementToolkit", "GmailToolkit", "JiraToolkit", diff --git a/libs/community/langchain_community/agent_toolkits/connery/__init__.py b/libs/community/langchain_community/agent_toolkits/connery/__init__.py new file mode 100644 index 0000000000..1839897c39 --- /dev/null +++ b/libs/community/langchain_community/agent_toolkits/connery/__init__.py @@ -0,0 +1,7 @@ +""" +This module contains the ConneryToolkit. +""" + +from .toolkit import ConneryToolkit + +__all__ = ["ConneryToolkit"] diff --git a/libs/community/langchain_community/agent_toolkits/connery/toolkit.py b/libs/community/langchain_community/agent_toolkits/connery/toolkit.py new file mode 100644 index 0000000000..03bbbf6231 --- /dev/null +++ b/libs/community/langchain_community/agent_toolkits/connery/toolkit.py @@ -0,0 +1,51 @@ +from typing import List + +from langchain_core.pydantic_v1 import root_validator +from langchain_core.tools import BaseTool + +from langchain_community.agent_toolkits.base import BaseToolkit +from langchain_community.tools.connery import ConneryService + + +class ConneryToolkit(BaseToolkit): + """ + A LangChain Toolkit with a list of Connery Actions as tools. + """ + + tools: List[BaseTool] + + def get_tools(self) -> List[BaseTool]: + """ + Returns the list of Connery Actions. + """ + return self.tools + + @root_validator() + def validate_attributes(cls, values: dict) -> dict: + """ + Validate the attributes of the ConneryToolkit class. + Parameters: + values (dict): The arguments to validate. + Returns: + dict: The validated arguments. + """ + + if not values.get("tools"): + raise ValueError("The attribute 'tools' must be set.") + + return values + + @classmethod + def create_instance(cls, connery_service: ConneryService) -> "ConneryToolkit": + """ + Creates a Connery Toolkit using a Connery Service. + Parameters: + connery_service (ConneryService): The Connery Service + to to get the list of Connery Actions. + Returns: + ConneryToolkit: The Connery Toolkit. + """ + + instance = cls(tools=connery_service.list_actions()) + + return instance diff --git a/libs/community/langchain_community/tools/__init__.py b/libs/community/langchain_community/tools/__init__.py index b00e023871..3456ef10bc 100644 --- a/libs/community/langchain_community/tools/__init__.py +++ b/libs/community/langchain_community/tools/__init__.py @@ -118,6 +118,12 @@ def _import_brave_search_tool() -> Any: return BraveSearch +def _import_connery_tool() -> Any: + from langchain_community.tools.connery import ConneryAction + + return ConneryAction + + def _import_ddg_search_tool_DuckDuckGoSearchResults() -> Any: from langchain_community.tools.ddg_search.tool import DuckDuckGoSearchResults @@ -797,6 +803,8 @@ def __getattr__(name: str) -> Any: return _import_bing_search_tool_BingSearchRun() elif name == "BraveSearch": return _import_brave_search_tool() + elif name == "ConneryAction": + return _import_connery_tool() elif name == "DuckDuckGoSearchResults": return _import_ddg_search_tool_DuckDuckGoSearchResults() elif name == "DuckDuckGoSearchRun": @@ -1035,6 +1043,7 @@ __all__ = [ "BingSearchRun", "BraveSearch", "ClickTool", + "ConneryAction", "CopyFileTool", "CurrentWebPageTool", "DeleteFileTool", diff --git a/libs/community/langchain_community/tools/connery/__init__.py b/libs/community/langchain_community/tools/connery/__init__.py new file mode 100644 index 0000000000..1fcf2760ba --- /dev/null +++ b/libs/community/langchain_community/tools/connery/__init__.py @@ -0,0 +1,8 @@ +""" +This module contains the ConneryAction Tool and ConneryService. +""" + +from .service import ConneryService +from .tool import ConneryAction + +__all__ = ["ConneryAction", "ConneryService"] diff --git a/libs/community/langchain_community/tools/connery/models.py b/libs/community/langchain_community/tools/connery/models.py new file mode 100644 index 0000000000..a8292e62b9 --- /dev/null +++ b/libs/community/langchain_community/tools/connery/models.py @@ -0,0 +1,32 @@ +from typing import List, Optional + +from langchain_core.pydantic_v1 import BaseModel + + +class Validation(BaseModel): + """Connery Action parameter validation model.""" + + required: Optional[bool] = None + + +class Parameter(BaseModel): + """Connery Action parameter model.""" + + key: str + title: str + description: Optional[str] = None + type: str + validation: Optional[Validation] = None + + +class Action(BaseModel): + """Connery Action model.""" + + id: str + key: str + title: str + description: Optional[str] = None + type: str + inputParameters: List[Parameter] + outputParameters: List[Parameter] + pluginId: str diff --git a/libs/community/langchain_community/tools/connery/service.py b/libs/community/langchain_community/tools/connery/service.py new file mode 100644 index 0000000000..decc9a440f --- /dev/null +++ b/libs/community/langchain_community/tools/connery/service.py @@ -0,0 +1,165 @@ +import json +from typing import Dict, List, Optional + +import requests +from langchain_core.pydantic_v1 import BaseModel, root_validator +from langchain_core.utils.env import get_from_dict_or_env + +from langchain_community.tools.connery.models import Action +from langchain_community.tools.connery.tool import ConneryAction + + +class ConneryService(BaseModel): + """ + A service for interacting with the Connery Runner API. + It gets the list of available actions from the Connery Runner, + wraps them in ConneryAction Tools and returns them to the user. + It also provides a method for running the actions. + """ + + runner_url: Optional[str] = None + api_key: Optional[str] = None + + @root_validator() + def validate_attributes(cls, values: Dict) -> Dict: + """ + Validate the attributes of the ConneryService class. + Parameters: + values (dict): The arguments to validate. + Returns: + dict: The validated arguments. + """ + + runner_url = get_from_dict_or_env(values, "runner_url", "CONNERY_RUNNER_URL") + api_key = get_from_dict_or_env(values, "api_key", "CONNERY_RUNNER_API_KEY") + + if not runner_url: + raise ValueError("CONNERY_RUNNER_URL environment variable must be set.") + if not api_key: + raise ValueError("CONNERY_RUNNER_API_KEY environment variable must be set.") + + values["runner_url"] = runner_url + values["api_key"] = api_key + + return values + + def list_actions(self) -> List[ConneryAction]: + """ + Returns the list of actions available in the Connery Runner. + Returns: + List[ConneryAction]: The list of actions available in the Connery Runner. + """ + + return [ + ConneryAction.create_instance(action, self) + for action in self._list_actions() + ] + + def get_action(self, action_id: str) -> ConneryAction: + """ + Returns the specified action available in the Connery Runner. + Parameters: + action_id (str): The ID of the action to return. + Returns: + ConneryAction: The action with the specified ID. + """ + + return ConneryAction.create_instance(self._get_action(action_id), self) + + def run_action(self, action_id: str, input: Dict[str, str] = {}) -> Dict[str, str]: + """ + Runs the specified Connery Action with the provided input. + Parameters: + action_id (str): The ID of the action to run. + input (Dict[str, str]): The input object expected by the action. + Returns: + Dict[str, str]: The output of the action. + """ + + return self._run_action(action_id, input) + + def _list_actions(self) -> List[Action]: + """ + Returns the list of actions available in the Connery Runner. + Returns: + List[Action]: The list of actions available in the Connery Runner. + """ + + response = requests.get( + f"{self.runner_url}/v1/actions", headers=self._get_headers() + ) + + if not response.ok: + raise ValueError( + ( + "Failed to list actions." + f"Status code: {response.status_code}." + f"Error message: {response.json()['error']['message']}" + ) + ) + + return [Action(**action) for action in response.json()["data"]] + + def _get_action(self, action_id: str) -> Action: + """ + Returns the specified action available in the Connery Runner. + Parameters: + action_id (str): The ID of the action to return. + Returns: + Action: The action with the specified ID. + """ + + actions = self._list_actions() + action = next((action for action in actions if action.id == action_id), None) + if not action: + raise ValueError( + ( + f"The action with ID {action_id} was not found in the list" + "of available actions in the Connery Runner." + ) + ) + return action + + def _run_action(self, action_id: str, input: Dict[str, str] = {}) -> Dict[str, str]: + """ + Runs the specified Connery Action with the provided input. + Parameters: + action_id (str): The ID of the action to run. + prompt (str): This is a plain English prompt + with all the information needed to run the action. + input (Dict[str, str]): The input object expected by the action. + If provided together with the prompt, + the input takes precedence over the input specified in the prompt. + Returns: + Dict[str, str]: The output of the action. + """ + + response = requests.post( + f"{self.runner_url}/v1/actions/{action_id}/run", + headers=self._get_headers(), + data=json.dumps({"input": input}), + ) + + if not response.ok: + raise ValueError( + ( + "Failed to run action." + f"Status code: {response.status_code}." + f"Error message: {response.json()['error']['message']}" + ) + ) + + if not response.json()["data"]["output"]: + return {} + else: + return response.json()["data"]["output"] + + def _get_headers(self) -> Dict[str, str]: + """ + Returns a standard set of HTTP headers + to be used in API calls to the Connery runner. + Returns: + Dict[str, str]: The standard set of HTTP headers. + """ + + return {"Content-Type": "application/json", "x-api-key": self.api_key or ""} diff --git a/libs/community/langchain_community/tools/connery/tool.py b/libs/community/langchain_community/tools/connery/tool.py new file mode 100644 index 0000000000..359a4dd75e --- /dev/null +++ b/libs/community/langchain_community/tools/connery/tool.py @@ -0,0 +1,163 @@ +import asyncio +from functools import partial +from typing import Any, Dict, List, Optional, Type + +from langchain_core.callbacks.manager import ( + AsyncCallbackManagerForToolRun, + CallbackManagerForToolRun, +) +from langchain_core.pydantic_v1 import BaseModel, Field, create_model, root_validator +from langchain_core.tools import BaseTool + +from langchain_community.tools.connery.models import Action, Parameter + + +class ConneryAction(BaseTool): + """ + A LangChain Tool wrapping a Connery Action. + """ + + name: str + description: str + args_schema: Type[BaseModel] + + action: Action + connery_service: Any + + def _run( + self, + run_manager: Optional[CallbackManagerForToolRun] = None, + **kwargs: Dict[str, str], + ) -> Dict[str, str]: + """ + Runs the Connery Action with the provided input. + Parameters: + kwargs (Dict[str, str]): The input dictionary expected by the action. + Returns: + Dict[str, str]: The output of the action. + """ + + return self.connery_service.run_action(self.action.id, kwargs) + + async def _arun( + self, + run_manager: Optional[AsyncCallbackManagerForToolRun] = None, + **kwargs: Dict[str, str], + ) -> Dict[str, str]: + """ + Runs the Connery Action asynchronously with the provided input. + Parameters: + kwargs (Dict[str, str]): The input dictionary expected by the action. + Returns: + Dict[str, str]: The output of the action. + """ + + func = partial(self._run, **kwargs) + return await asyncio.get_event_loop().run_in_executor(None, func) + + def get_schema_json(self) -> str: + """ + Returns the JSON representation of the Connery Action Tool schema. + This is useful for debugging. + Returns: + str: The JSON representation of the Connery Action Tool schema. + """ + + return self.args_schema.schema_json(indent=2) + + @root_validator() + def validate_attributes(cls, values: dict) -> dict: + """ + Validate the attributes of the ConneryAction class. + Parameters: + values (dict): The arguments to validate. + Returns: + dict: The validated arguments. + """ + + # Import ConneryService here and check if it is an instance + # of ConneryService to avoid circular imports + from .service import ConneryService + + if not isinstance(values.get("connery_service"), ConneryService): + raise ValueError( + "The attribute 'connery_service' must be an instance of ConneryService." + ) + + if not values.get("name"): + raise ValueError("The attribute 'name' must be set.") + if not values.get("description"): + raise ValueError("The attribute 'description' must be set.") + if not values.get("args_schema"): + raise ValueError("The attribute 'args_schema' must be set.") + if not values.get("action"): + raise ValueError("The attribute 'action' must be set.") + if not values.get("connery_service"): + raise ValueError("The attribute 'connery_service' must be set.") + + return values + + @classmethod + def create_instance(cls, action: Action, connery_service: Any) -> "ConneryAction": + """ + Creates a Connery Action Tool from a Connery Action. + Parameters: + action (Action): The Connery Action to wrap in a Connery Action Tool. + connery_service (ConneryService): The Connery Service + to run the Connery Action. We use Any here to avoid circular imports. + Returns: + ConneryAction: The Connery Action Tool. + """ + + # Import ConneryService here and check if it is an instance + # of ConneryService to avoid circular imports + from .service import ConneryService + + if not isinstance(connery_service, ConneryService): + raise ValueError( + "The connery_service must be an instance of ConneryService." + ) + + input_schema = cls._create_input_schema(action.inputParameters) + description = action.title + ( + ": " + action.description if action.description else "" + ) + + instance = cls( + name=action.id, + description=description, + args_schema=input_schema, + action=action, + connery_service=connery_service, + ) + + return instance + + @classmethod + def _create_input_schema(cls, inputParameters: List[Parameter]) -> Type[BaseModel]: + """ + Creates an input schema for a Connery Action Tool + based on the input parameters of the Connery Action. + Parameters: + inputParameters: List of input parameters of the Connery Action. + Returns: + Type[BaseModel]: The input schema for the Connery Action Tool. + """ + + dynamic_input_fields: Dict[str, Any] = {} + + for param in inputParameters: + default = ... if param.validation and param.validation.required else None + title = param.title + description = param.title + ( + ": " + param.description if param.description else "" + ) + type = param.type + + dynamic_input_fields[param.key] = ( + str, + Field(default, title=title, description=description, type=type), + ) + + InputModel = create_model("InputSchema", **dynamic_input_fields) + return InputModel diff --git a/libs/community/tests/integration_tests/tools/connery/test_service.py b/libs/community/tests/integration_tests/tools/connery/test_service.py new file mode 100644 index 0000000000..3771d3100c --- /dev/null +++ b/libs/community/tests/integration_tests/tools/connery/test_service.py @@ -0,0 +1,41 @@ +"""Integration test for Connery API Wrapper.""" +from langchain_community.tools.connery import ConneryService + + +def test_list_actions() -> None: + """Test for listing Connery Actions.""" + connery = ConneryService() + output = connery._list_actions() + assert output is not None + assert len(output) > 0 + + +def test_get_action() -> None: + """Test for getting Connery Action.""" + connery = ConneryService() + # This is the ID of the preinstalled action "Refresh plugin cache" + output = connery._get_action("CAF979E6D2FF4C8B946EEBAFCB3BA475") + assert output is not None + assert output.id == "CAF979E6D2FF4C8B946EEBAFCB3BA475" + + +def test_run_action_with_no_iput() -> None: + """Test for running Connery Action without input.""" + connery = ConneryService() + # refreshPluginCache action from connery-io/connery-runner-administration plugin + output = connery._run_action("CAF979E6D2FF4C8B946EEBAFCB3BA475") + assert output is not None + assert output == {} + + +def test_run_action_with_iput() -> None: + """Test for running Connery Action with input.""" + connery = ConneryService() + # summarizePublicWebpage action from connery-io/summarization-plugin plugin + output = connery._run_action( + "CA72DFB0AB4DF6C830B43E14B0782F70", + {"publicWebpageUrl": "http://www.paulgraham.com/vb.html"}, + ) + assert output is not None + assert output["summary"] is not None + assert len(output["summary"]) > 0 diff --git a/libs/community/tests/unit_tests/agent_toolkits/test_imports.py b/libs/community/tests/unit_tests/agent_toolkits/test_imports.py index 416b66849b..3a7ca10efd 100644 --- a/libs/community/tests/unit_tests/agent_toolkits/test_imports.py +++ b/libs/community/tests/unit_tests/agent_toolkits/test_imports.py @@ -4,6 +4,7 @@ EXPECTED_ALL = [ "AINetworkToolkit", "AmadeusToolkit", "AzureCognitiveServicesToolkit", + "ConneryToolkit", "FileManagementToolkit", "GmailToolkit", "JiraToolkit", diff --git a/libs/community/tests/unit_tests/tools/test_imports.py b/libs/community/tests/unit_tests/tools/test_imports.py index 9fdcf157e9..4bf70aa084 100644 --- a/libs/community/tests/unit_tests/tools/test_imports.py +++ b/libs/community/tests/unit_tests/tools/test_imports.py @@ -24,6 +24,7 @@ EXPECTED_ALL = [ "BingSearchRun", "BraveSearch", "ClickTool", + "ConneryAction", "CopyFileTool", "CurrentWebPageTool", "DeleteFileTool", diff --git a/libs/community/tests/unit_tests/tools/test_public_api.py b/libs/community/tests/unit_tests/tools/test_public_api.py index 0f6102c45e..31ea832702 100644 --- a/libs/community/tests/unit_tests/tools/test_public_api.py +++ b/libs/community/tests/unit_tests/tools/test_public_api.py @@ -25,6 +25,7 @@ _EXPECTED = [ "BingSearchRun", "BraveSearch", "ClickTool", + "ConneryAction", "CopyFileTool", "CurrentWebPageTool", "DeleteFileTool",