From d4edd3c312ae5edefded240d42da86532f4ca4cc Mon Sep 17 00:00:00 2001 From: Ankush Gola <9536492+agola11@users.noreply.github.com> Date: Tue, 14 Mar 2023 23:06:17 -0700 Subject: [PATCH] Zapier Integration (#1654) * Zapier Wrapper and Tools (implemented by Zapier Team) * Zapier Toolkit, examples with mrkl agent --------- Co-authored-by: Mike Knoop Co-authored-by: Robert Lewis --- .gitignore | 3 + docs/modules/utils/examples/requests.ipynb | 8 +- docs/modules/utils/examples/zapier.ipynb | 326 ++++++++++++++++++ langchain/agents/agent_toolkits/__init__.py | 2 + .../agents/agent_toolkits/zapier/__init__.py | 1 + .../agents/agent_toolkits/zapier/toolkit.py | 34 ++ langchain/tools/zapier/__init__.py | 1 + langchain/tools/zapier/prompt.py | 15 + langchain/tools/zapier/tool.py | 159 +++++++++ langchain/utilities/zapier.py | 155 +++++++++ 10 files changed, 700 insertions(+), 4 deletions(-) create mode 100644 docs/modules/utils/examples/zapier.ipynb create mode 100644 langchain/agents/agent_toolkits/zapier/__init__.py create mode 100644 langchain/agents/agent_toolkits/zapier/toolkit.py create mode 100644 langchain/tools/zapier/__init__.py create mode 100644 langchain/tools/zapier/prompt.py create mode 100644 langchain/tools/zapier/tool.py create mode 100644 langchain/utilities/zapier.py diff --git a/.gitignore b/.gitignore index 0b7bffc8996cc..5bdf0d1b533ae 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,6 @@ dmypy.json # macOS display setting files .DS_Store + +# asdf tool versions +.tool-versions \ No newline at end of file diff --git a/docs/modules/utils/examples/requests.ipynb b/docs/modules/utils/examples/requests.ipynb index b433de5de7efb..7096f1383f626 100644 --- a/docs/modules/utils/examples/requests.ipynb +++ b/docs/modules/utils/examples/requests.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "id": "81aae09e", "metadata": {}, "outputs": [], @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "fd210142", "metadata": {}, "outputs": [], @@ -39,7 +39,7 @@ { "data": { "text/plain": [ - "'Google

\"Seasonal
 

Advanced search

© 2022 - Privacy - Terms

'" + "'Google

\"International

 

Advanced search

Celebrate International Women\\'s Day with Google

© 2023 - Privacy - Terms

'" ] }, "execution_count": 5, @@ -76,7 +76,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/docs/modules/utils/examples/zapier.ipynb b/docs/modules/utils/examples/zapier.ipynb new file mode 100644 index 0000000000000..ea4bed063ea6c --- /dev/null +++ b/docs/modules/utils/examples/zapier.ipynb @@ -0,0 +1,326 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "16763ed3", + "metadata": {}, + "source": [ + "## Zapier Natural Language Actions API\n", + "\\\n", + "Full docs here: https://nla.zapier.com/api/v1/dynamic/docs\n", + "\n", + "**Zapier Natural Language Actions** gives you access to the 5k+ apps, 20k+ actions on Zapier's platform through a natural language API interface.\n", + "\n", + "NLA supports apps like Gmail, Salesforce, Trello, Slack, Asana, HubSpot, Google Sheets, Microsoft Teams, and thousands more apps: https://zapier.com/apps\n", + "\n", + "Zapier NLA handles ALL the underlying API auth and translation from natural language --> underlying API call --> return simplified output for LLMs. The key idea is you, or your users, expose a set of actions via an oauth-like setup window, which you can then query and execute via a REST API.\n", + "\n", + "NLA offers both API Key and OAuth for signing NLA API requests.\n", + "\n", + "1. Server-side (API Key): for quickly getting started, testing, and production scenarios where LangChain will only use actions exposed in the developer's Zapier account (and will use the developer's connected accounts on Zapier.com)\n", + "\n", + "2. User-facing (Oauth): for production scenarios where you are deploying an end-user facing application and LangChain needs access to end-user's exposed actions and connected accounts on Zapier.com\n", + "\n", + "This quick start will focus on the server-side use case for brevity. Review [full docs](https://nla.zapier.com/api/v1/dynamic/docs) or reach out to nla@zapier.com for user-facing oauth developer support.\n", + "\n", + "This example goes over how to use the Zapier integration with a `SimpleSequentialChain`, then an `Agent`.\n", + "In code, below:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a363309c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5cf33377", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# get from https://platform.openai.com/\n", + "os.environ[\"OPENAI_API_KEY\"] = os.environ.get(\"OPENAI_API_KEY\", \"\")\n", + "\n", + "# get from https://nla.zapier.com/demo/provider/debug (under User Information, after logging in): \n", + "os.environ[\"ZAPIER_NLA_API_KEY\"] = os.environ.get(\"ZAPIER_NLA_API_KEY\", \"\")" + ] + }, + { + "cell_type": "markdown", + "id": "4881b484-1b97-478f-b206-aec407ceff66", + "metadata": {}, + "source": [ + "## Example with Agent\n", + "Zapier tools can be used with an agent. See the example below." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b2044b17-c941-4ffb-8a03-027a35e2df81", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain.llms import OpenAI\n", + "from langchain.agents import initialize_agent\n", + "from langchain.agents.agent_toolkits import ZapierToolkit\n", + "from langchain.utilities.zapier import ZapierNLAWrapper" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7b505eeb", + "metadata": {}, + "outputs": [], + "source": [ + "## step 0. expose gmail 'find email' and slack 'send channel message' actions\n", + "\n", + "# first go here, log in, expose (enable) the two actions: https://nla.zapier.com/demo/start -- for this example, can leave all fields \"Have AI guess\"\n", + "# in an oauth scenario, you'd get your own id (instead of 'demo') which you route your users through first" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cab18227-c232-4214-9256-bb8dd352266c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "llm = OpenAI(temperature=0)\n", + "zapier = ZapierNLAWrapper()\n", + "toolkit = ZapierToolkit.from_zapier_nla_wrapper(zapier)\n", + "agent = initialize_agent(toolkit.get_tools(), llm, agent=\"zero-shot-react-description\", verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f94713de-b64d-465f-a087-00288b5f80ec", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m I need to find the email and summarize it.\n", + "Action: Gmail: Find Email\n", + "Action Input: Find the latest email from Silicon Valley Bank\u001b[0m\n", + "Observation: \u001b[31;1m\u001b[1;3m{\"from__name\": \"Silicon Valley Bridge Bank, N.A.\", \"from__email\": \"sreply@svb.com\", \"body_plain\": \"Dear Clients, After chaotic, tumultuous & stressful days, we have clarity on path for SVB, FDIC is fully insuring all deposits & have an ask for clients & partners as we rebuild. Tim Mayopoulos Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'I have sent a summary of the last email from Silicon Valley Bank to the #test-zapier channel in Slack.'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.run(\"Summarize the last email I received regarding Silicon Valley Bank. Send the summary to the #test-zapier channel in slack.\")" + ] + }, + { + "cell_type": "markdown", + "id": "bcdea831", + "metadata": {}, + "source": [ + "# Example with SimpleSequentialChain\n", + "If you need more explicit control, use a chain, like below." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "10a46e7e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from langchain.llms import OpenAI\n", + "from langchain.chains import LLMChain, TransformChain, SimpleSequentialChain\n", + "from langchain.prompts import PromptTemplate\n", + "from langchain.tools.zapier.tool import ZapierNLARunAction\n", + "from langchain.utilities.zapier import ZapierNLAWrapper" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b9358048", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "## step 0. expose gmail 'find email' and slack 'send direct message' actions\n", + "\n", + "# first go here, log in, expose (enable) the two actions: https://nla.zapier.com/demo/start -- for this example, can leave all fields \"Have AI guess\"\n", + "# in an oauth scenario, you'd get your own id (instead of 'demo') which you route your users through first\n", + "\n", + "actions = ZapierNLAWrapper().list()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4e80f461", + "metadata": {}, + "outputs": [], + "source": [ + "## step 1. gmail find email\n", + "\n", + "GMAIL_SEARCH_INSTRUCTIONS = \"Grab the latest email from Silicon Valley Bank\"\n", + "\n", + "def nla_gmail(inputs):\n", + " action = next((a for a in actions if a[\"description\"].startswith(\"Gmail: Find Email\")), None)\n", + " return {\"email_data\": ZapierNLARunAction(action_id=action[\"id\"], zapier_description=action[\"description\"], params_schema=action[\"params\"]).run(inputs[\"instructions\"])}\n", + "gmail_chain = TransformChain(input_variables=[\"instructions\"], output_variables=[\"email_data\"], transform=nla_gmail)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "46893233", + "metadata": {}, + "outputs": [], + "source": [ + "## step 2. generate draft reply\n", + "\n", + "template = \"\"\"You are an assisstant who drafts replies to an incoming email. Output draft reply in plain text (not JSON).\n", + "\n", + "Incoming email:\n", + "{email_data}\n", + "\n", + "Draft email reply:\"\"\"\n", + "\n", + "prompt_template = PromptTemplate(input_variables=[\"email_data\"], template=template)\n", + "reply_chain = LLMChain(llm=OpenAI(temperature=.7), prompt=prompt_template)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cd85c4f8", + "metadata": {}, + "outputs": [], + "source": [ + "## step 3. send draft reply via a slack direct message\n", + "\n", + "SLACK_HANDLE = \"@Ankush Gola\"\n", + "\n", + "def nla_slack(inputs):\n", + " action = next((a for a in actions if a[\"description\"].startswith(\"Slack: Send Direct Message\")), None)\n", + " instructions = f'Send this to {SLACK_HANDLE} in Slack: {inputs[\"draft_reply\"]}'\n", + " return {\"slack_data\": ZapierNLARunAction(action_id=action[\"id\"], zapier_description=action[\"description\"], params_schema=action[\"params\"]).run(instructions)}\n", + "slack_chain = TransformChain(input_variables=[\"draft_reply\"], output_variables=[\"slack_data\"], transform=nla_slack)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4829cab4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new SimpleSequentialChain chain...\u001b[0m\n", + "\u001b[36;1m\u001b[1;3m{\"from__name\": \"Silicon Valley Bridge Bank, N.A.\", \"from__email\": \"sreply@svb.com\", \"body_plain\": \"Dear Clients, After chaotic, tumultuous & stressful days, we have clarity on path for SVB, FDIC is fully insuring all deposits & have an ask for clients & partners as we rebuild. Tim Mayopoulos Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'{\"message__text\": \"Dear Silicon Valley Bridge Bank, \\\\n\\\\nThank you for your email and the update regarding your new CEO Tim Mayopoulos. We appreciate your dedication to keeping your clients and partners informed and we look forward to continuing our relationship with you. \\\\n\\\\nBest regards, \\\\n[Your Name]\", \"message__permalink\": \"https://langchain.slack.com/archives/D04TKF5BBHU/p1678859968241629\", \"channel\": \"D04TKF5BBHU\", \"message__bot_profile__name\": \"Zapier\", \"message__team\": \"T04F8K3FZB5\", \"message__bot_id\": \"B04TRV4R74K\", \"message__bot_profile__deleted\": \"false\", \"message__bot_profile__app_id\": \"A024R9PQM\", \"ts_time\": \"2023-03-15T05:59:28Z\", \"message__blocks[]block_id\": \"p7i\", \"message__blocks[]elements[]elements[]type\": \"[[\\'text\\']]\", \"message__blocks[]elements[]type\": \"[\\'rich_text_section\\']\"}'" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## finally, execute\n", + "\n", + "overall_chain = SimpleSequentialChain(chains=[gmail_chain, reply_chain, slack_chain], verbose=True)\n", + "overall_chain.run(GMAIL_SEARCH_INSTRUCTIONS)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09ff954e-45f2-4595-92ea-91627abde4a0", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/agents/agent_toolkits/__init__.py b/langchain/agents/agent_toolkits/__init__.py index 0f49258b8d909..0b65e01d24df5 100644 --- a/langchain/agents/agent_toolkits/__init__.py +++ b/langchain/agents/agent_toolkits/__init__.py @@ -18,6 +18,7 @@ VectorStoreRouterToolkit, VectorStoreToolkit, ) +from langchain.agents.agent_toolkits.zapier.toolkit import ZapierToolkit __all__ = [ "create_json_agent", @@ -34,4 +35,5 @@ "VectorStoreRouterToolkit", "create_pandas_dataframe_agent", "create_csv_agent", + "ZapierToolkit", ] diff --git a/langchain/agents/agent_toolkits/zapier/__init__.py b/langchain/agents/agent_toolkits/zapier/__init__.py new file mode 100644 index 0000000000000..faef4a3253167 --- /dev/null +++ b/langchain/agents/agent_toolkits/zapier/__init__.py @@ -0,0 +1 @@ +"""Zapier Toolkit.""" diff --git a/langchain/agents/agent_toolkits/zapier/toolkit.py b/langchain/agents/agent_toolkits/zapier/toolkit.py new file mode 100644 index 0000000000000..47e27ce714129 --- /dev/null +++ b/langchain/agents/agent_toolkits/zapier/toolkit.py @@ -0,0 +1,34 @@ +"""Zapier Toolkit.""" +from typing import List + +from langchain.agents.agent_toolkits.base import BaseToolkit +from langchain.tools import BaseTool +from langchain.tools.zapier.tool import ZapierNLARunAction +from langchain.utilities.zapier import ZapierNLAWrapper + + +class ZapierToolkit(BaseToolkit): + """Zapier Toolkit.""" + + tools: List[BaseTool] = [] + + @classmethod + def from_zapier_nla_wrapper( + cls, zapier_nla_wrapper: ZapierNLAWrapper + ) -> "ZapierToolkit": + """Create a toolkit from a ZapierNLAWrapper.""" + actions = zapier_nla_wrapper.list() + tools = [ + ZapierNLARunAction( + action_id=action["id"], + zapier_description=action["description"], + params_schema=action["params"], + api_wrapper=zapier_nla_wrapper, + ) + for action in actions + ] + return cls(tools=tools) + + def get_tools(self) -> List[BaseTool]: + """Get the tools in the toolkit.""" + return self.tools diff --git a/langchain/tools/zapier/__init__.py b/langchain/tools/zapier/__init__.py new file mode 100644 index 0000000000000..ef6b8a2aa64e3 --- /dev/null +++ b/langchain/tools/zapier/__init__.py @@ -0,0 +1 @@ +"""Zapier Tool.""" diff --git a/langchain/tools/zapier/prompt.py b/langchain/tools/zapier/prompt.py new file mode 100644 index 0000000000000..063e3952ef2aa --- /dev/null +++ b/langchain/tools/zapier/prompt.py @@ -0,0 +1,15 @@ +# flake8: noqa +BASE_ZAPIER_TOOL_PROMPT = ( + "A wrapper around Zapier NLA actions. " + "The input to this tool is a natural language instruction, " + 'for example "get the latest email from my bank" or ' + '"send a slack message to the #general channel". ' + "Each tool will have params associated with it that are specified as a list. You MUST take into account the params when creating the instruction. " + "For example, if the params are ['Message_Text', 'Channel'], your instruction should be something like 'send a slack message to the #general channel with the text hello world'. " + "Another example: if the params are ['Calendar', 'Search_Term'], your instruction should be something like 'find the meeting in my personal calendar at 3pm'. " + "Do not make up params, they will be explicitly specified in the tool description. " + "If you do not have enough information to fill in the params, just say 'not enough information provided in the instruction, missing '. " + "If you get a none or null response, STOP EXECUTION, do not try to another tool!" + "This tool specifically used for: {zapier_description}, " + "and has params: {params}" +) diff --git a/langchain/tools/zapier/tool.py b/langchain/tools/zapier/tool.py new file mode 100644 index 0000000000000..a3c3be322a6f5 --- /dev/null +++ b/langchain/tools/zapier/tool.py @@ -0,0 +1,159 @@ +"""## Zapier Natural Language Actions API +\ +Full docs here: https://nla.zapier.com/api/v1/dynamic/docs + +**Zapier Natural Language Actions** gives you access to the 5k+ apps, 20k+ actions +on Zapier's platform through a natural language API interface. + +NLA supports apps like Gmail, Salesforce, Trello, Slack, Asana, HubSpot, Google Sheets, +Microsoft Teams, and thousands more apps: https://zapier.com/apps + +Zapier NLA handles ALL the underlying API auth and translation from +natural language --> underlying API call --> return simplified output for LLMs +The key idea is you, or your users, expose a set of actions via an oauth-like setup +window, which you can then query and execute via a REST API. + +NLA offers both API Key and OAuth for signing NLA API requests. + +1. Server-side (API Key): for quickly getting started, testing, and production scenarios + where LangChain will only use actions exposed in the developer's Zapier account + (and will use the developer's connected accounts on Zapier.com) + +2. User-facing (Oauth): for production scenarios where you are deploying an end-user + facing application and LangChain needs access to end-user's exposed actions and + connected accounts on Zapier.com + +This quick start will focus on the server-side use case for brevity. +Review [full docs](https://nla.zapier.com/api/v1/dynamic/docs) or reach out to +nla@zapier.com for user-facing oauth developer support. + +Typically you'd use SequentialChain, here's a basic example: + + 1. Use NLA to find an email in Gmail + 2. Use LLMChain to generate a draft reply to (1) + 3. Use NLA to send the draft reply (2) to someone in Slack via direct mesage + +In code, below: + +```python + +import os + +# get from https://platform.openai.com/ +os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY", "") + +# get from https://nla.zapier.com/demo/provider/debug +# (under User Information, after logging in): +os.environ["ZAPIER_NLA_API_KEY"] = os.environ.get("ZAPIER_NLA_API_KEY", "") + +from langchain.llms import OpenAI +from langchain.agents import initialize_agent +from langchain.agents.agent_toolkits import ZapierToolkit +from langchain.utilities.zapier import ZapierNLAWrapper + +## step 0. expose gmail 'find email' and slack 'send channel message' actions + +# first go here, log in, expose (enable) the two actions: +# https://nla.zapier.com/demo/start +# -- for this example, can leave all fields "Have AI guess" +# in an oauth scenario, you'd get your own id (instead of 'demo') +# which you route your users through first + +llm = OpenAI(temperature=0) +zapier = ZapierNLAWrapper() +toolkit = ZapierToolkit.from_zapier_nla_wrapper(zapier) +agent = initialize_agent( + toolkit.get_tools(), + llm, + agent="zero-shot-react-description", + verbose=True +) + +agent.run(("Summarize the last email I received regarding Silicon Valley Bank. " + "Send the summary to the #test-zapier channel in slack.")) +``` + +""" +from typing import Any, Dict, Optional + +from pydantic import Field, root_validator + +from langchain.tools.base import BaseTool +from langchain.tools.zapier.prompt import BASE_ZAPIER_TOOL_PROMPT +from langchain.utilities.zapier import ZapierNLAWrapper + + +class ZapierNLARunAction(BaseTool): + """ + Args: + action_id: a specific action ID (from list actions) of the action to execute + (the set api_key must be associated with the action owner) + instructions: a natural language instruction string for using the action + (eg. "get the latest email from Mike Knoop" for "Gmail: find email" action) + params: a dict, optional. Any params provided will *override* AI guesses + from `instructions` (see "understanding the AI guessing flow" here: + https://nla.zapier.com/api/v1/dynamic/docs) + """ + + api_wrapper: ZapierNLAWrapper = Field(default_factory=ZapierNLAWrapper) + action_id: str + params: Optional[dict] = None + zapier_description: str + params_schema: Dict[str, str] = Field(default_factory=dict) + name = "" + description = "" + + @root_validator + def set_name_description(cls, values: Dict[str, Any]) -> Dict[str, Any]: + zapier_description = values["zapier_description"] + params_schema = values["params_schema"] + if "instructions" in params_schema: + del params_schema["instructions"] + values["name"] = zapier_description + values["description"] = BASE_ZAPIER_TOOL_PROMPT.format( + zapier_description=zapier_description, + params=str(list(params_schema.keys())), + ) + return values + + def _run(self, instructions: str) -> str: + """Use the Zapier NLA tool to return a list of all exposed user actions.""" + return self.api_wrapper.run_as_str(self.action_id, instructions, self.params) + + async def _arun(self, _: str) -> str: + """Use the Zapier NLA tool to return a list of all exposed user actions.""" + raise NotImplementedError("ZapierNLAListActions does not support async") + + +ZapierNLARunAction.__doc__ = ( + ZapierNLAWrapper.run.__doc__ + ZapierNLARunAction.__doc__ # type: ignore +) + + +# other useful actions + + +class ZapierNLAListActions(BaseTool): + """ + Args: + None + """ + + name = "Zapier NLA: List Actions" + description = BASE_ZAPIER_TOOL_PROMPT + ( + "This tool returns a list of the user's exposed actions." + ) + api_wrapper: ZapierNLAWrapper = Field(default_factory=ZapierNLAWrapper) + + def _run(self, _: str) -> str: + """Use the Zapier NLA tool to return a list of all exposed user actions.""" + return self.api_wrapper.list_as_str() + + async def _arun(self, _: str) -> str: + """Use the Zapier NLA tool to return a list of all exposed user actions.""" + raise NotImplementedError("ZapierNLAListActions does not support async") + + +ZapierNLAListActions.__doc__ = ( + ZapierNLAWrapper.list.__doc__ + ZapierNLAListActions.__doc__ # type: ignore +) diff --git a/langchain/utilities/zapier.py b/langchain/utilities/zapier.py new file mode 100644 index 0000000000000..0ffb04f99ccde --- /dev/null +++ b/langchain/utilities/zapier.py @@ -0,0 +1,155 @@ +"""Util that can interact with Zapier NLA. + +Full docs here: https://nla.zapier.com/api/v1/dynamic/docs + +Note: this wrapper currently only implemented the `api_key` auth method for testing +and server-side production use cases (using the developer's connected accounts on +Zapier.com) + +For use-cases where LangChain + Zapier NLA is powering a user-facing application, and +LangChain needs access to the end-user's connected accounts on Zapier.com, you'll need +to use oauth. Review the full docs above and reach out to nla@zapier.com for +developer support. +""" +import json +from typing import Dict, List, Optional + +import requests +from pydantic import BaseModel, Extra, root_validator +from requests import Request, Session + +from langchain.utils import get_from_dict_or_env + + +class ZapierNLAWrapper(BaseModel): + """Wrapper for Zapier NLA. + + Full docs here: https://nla.zapier.com/api/v1/dynamic/docs + + Note: this wrapper currently only implemented the `api_key` auth method for + testingand server-side production use cases (using the developer's connected + accounts on Zapier.com) + + For use-cases where LangChain + Zapier NLA is powering a user-facing application, + and LangChain needs access to the end-user's connected accounts on Zapier.com, + you'll need to use oauth. Review the full docs above and reach out to + nla@zapier.com for developer support. + """ + + zapier_nla_api_key: str + zapier_nla_api_base: str = "https://nla.zapier.com/api/v1/" + zapier_nla_api_dynamic_base: str = "https://nla.zapier.com/api/v1/dynamic/" + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + + def _get_session(self) -> Session: + session = requests.Session() + session.headers.update( + { + "Accept": "application/json", + "Content-Type": "application/json", + } + ) + session.params = {"api_key": self.zapier_nla_api_key} + return session + + def _get_action_request( + self, action_id: str, instructions: str, params: Optional[Dict] = None + ) -> Request: + data = params if params else {} + data.update( + { + "instructions": instructions, + } + ) + return Request( + "POST", + self.zapier_nla_api_base + f"exposed/{action_id}/execute/", + json=data, + ) + + @root_validator(pre=True) + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key exists in environment.""" + zapier_nla_api_key = get_from_dict_or_env( + values, "zapier_nla_api_key", "ZAPIER_NLA_API_KEY" + ) + values["zapier_nla_api_key"] = zapier_nla_api_key + + return values + + def list(self) -> List[Dict]: + """Returns a list of all exposed (enabled) actions associated with + current user (associated with the set api_key). Change your exposed + actions here: https://nla.zapier.com/demo/start/ + + The return list can be empty if no actions exposed. Else will contain + a list of action objects: + + [{ + "id": str, + "description": str, + "params": Dict[str, str] + }] + + `params` will always contain an `instructions` key, the only required + param. All others optional and if provided will override any AI guesses + (see "understanding the AI guessing flow" here: + https://nla.zapier.com/api/v1/dynamic/docs) + """ + session = self._get_session() + response = session.get(self.zapier_nla_api_dynamic_base + "exposed/") + response.raise_for_status() + return response.json()["results"] + + def run( + self, action_id: str, instructions: str, params: Optional[Dict] = None + ) -> Dict: + """Executes an action that is identified by action_id, must be exposed + (enabled) by the current user (associated with the set api_key). Change + your exposed actions here: https://nla.zapier.com/demo/start/ + + The return JSON is guaranteed to be less than ~500 words (350 + tokens) making it safe to inject into the prompt of another LLM + call. + """ + session = self._get_session() + request = self._get_action_request(action_id, instructions, params) + response = session.send(session.prepare_request(request)) + response.raise_for_status() + return response.json()["result"] + + def preview( + self, action_id: str, instructions: str, params: Optional[Dict] = None + ) -> Dict: + """Same as run, but instead of actually executing the action, will + instead return a preview of params that have been guessed by the AI in + case you need to explicitly review before executing.""" + session = self._get_session() + params = params if params else {} + params.update({"preview_only": True}) + request = self._get_action_request(action_id, instructions, params) + response = session.send(session.prepare_request(request)) + response.raise_for_status() + return response.json()["input_params"] + + def run_as_str(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Same as run, but returns a stringified version of the JSON for + insertting back into an LLM.""" + data = self.run(*args, **kwargs) + return json.dumps(data) + + def preview_as_str(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Same as preview, but returns a stringified version of the JSON for + insertting back into an LLM.""" + data = self.preview(*args, **kwargs) + return json.dumps(data) + + def list_as_str(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Same as list, but returns a stringified version of the JSON for + insertting back into an LLM.""" + actions = self.list(*args, **kwargs) + return json.dumps(actions)