From 9b9b231e10725ed2f1ebccffd5524276aa3018ea Mon Sep 17 00:00:00 2001 From: Zander Chase <130414180+vowelparrot@users.noreply.github.com> Date: Mon, 1 May 2023 19:07:26 -0700 Subject: [PATCH] Update some Tools Docs (#3913) Haven't gotten to all of them, but this: - Updates some of the tools notebooks to actually instantiate a tool (many just show a 'utility' rather than a tool. More changes to come in separate PR) - Move the `Tool` and decorator definitions to `langchain/tools/base.py` (but still export from `langchain.agents`) - Add scene explain to the load_tools() function - Add unit tests for public apis for the langchain.tools and langchain.agents modules --- docs/modules/agents/tools/custom_tools.ipynb | 349 +++++++++++++----- .../agents/tools/examples/google_search.ipynb | 93 ++--- .../agents/tools/examples/requests.ipynb | 75 +++- .../agents/tools/examples/sceneXplain.ipynb | 33 +- langchain/agents/__init__.py | 44 +-- langchain/agents/load_tools.py | 6 + langchain/agents/tools.py | 170 +-------- langchain/tools/__init__.py | 15 +- langchain/tools/base.py | 185 ++++++++++ langchain/tools/plugin.py | 4 +- langchain/utilities/scenexplain.py | 6 +- tests/unit_tests/agents/test_public_api.py | 38 ++ tests/unit_tests/tools/test_exported.py | 35 ++ tests/unit_tests/tools/test_public_api.py | 51 +++ 14 files changed, 746 insertions(+), 358 deletions(-) create mode 100644 tests/unit_tests/agents/test_public_api.py create mode 100644 tests/unit_tests/tools/test_exported.py create mode 100644 tests/unit_tests/tools/test_public_api.py diff --git a/docs/modules/agents/tools/custom_tools.ipynb b/docs/modules/agents/tools/custom_tools.ipynb index 050580df..a0fea125 100644 --- a/docs/modules/agents/tools/custom_tools.ipynb +++ b/docs/modules/agents/tools/custom_tools.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "id": "5436020b", "metadata": {}, @@ -12,11 +13,10 @@ "- name (str), is required and must be unique within a set of tools provided to an agent\n", "- description (str), is optional but recommended, as it is used by an agent to determine tool use\n", "- return_direct (bool), defaults to False\n", - "- args_schema (Pydantic BaseModel), is optional but recommended, can be used to provide more information or validation for expected parameters.\n", + "- args_schema (Pydantic BaseModel), is optional but recommended, can be used to provide more information (e.g., few-shot examples) or validation for expected parameters.\n", "\n", - "The function that should be called when the tool is selected should return a single string.\n", "\n", - "There are two ways to define a tool, we will cover both in the example below." + "There are two main ways to define a tool, we will cover both in the example below." ] }, { @@ -30,9 +30,9 @@ "source": [ "# Import things that are needed generically\n", "from langchain import LLMMathChain, SerpAPIWrapper\n", - "from langchain.agents import AgentType, Tool, initialize_agent, tool\n", + "from langchain.agents import AgentType, initialize_agent\n", "from langchain.chat_models import ChatOpenAI\n", - "from langchain.tools import BaseTool" + "from langchain.tools import BaseTool, StructuredTool, Tool, tool" ] }, { @@ -56,22 +56,27 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "id": "f8bc72c2", "metadata": {}, "source": [ - "## Completely New Tools \n", - "First, we show how to create completely new tools from scratch.\n", + "## Completely New Tools - String Input and Output\n", + "\n", + "The simplest tools accept a single query string and return a string output. If your tool function requires multiple arguments, you might want to skip down to the `StructuredTool` section below.\n", "\n", "There are two ways to do this: either by using the Tool dataclass, or by subclassing the BaseTool class." ] }, { + "attachments": {}, "cell_type": "markdown", "id": "b63fcc3b", "metadata": {}, "source": [ - "### Tool dataclass" + "### Tool dataclass\n", + "\n", + "The 'Tool' dataclass wraps functions that accept a single string input and returns a string output." ] }, { @@ -81,19 +86,46 @@ "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/wfh/code/lc/lckg/langchain/chains/llm_math/base.py:50: UserWarning: Directly instantiating an LLMMathChain with an llm is deprecated. Please instantiate with llm_chain argument or using the from_llm class method.\n", + " warnings.warn(\n" + ] + } + ], "source": [ "# Load the tool configs that are needed.\n", "search = SerpAPIWrapper()\n", "llm_math_chain = LLMMathChain(llm=llm, verbose=True)\n", "tools = [\n", - " Tool(\n", - " name = \"Search\",\n", + " Tool.from_function(\n", " func=search.run,\n", + " name = \"Search\",\n", " description=\"useful for when you need to answer questions about current events\"\n", + " # coroutine= ... <- you can specify an async method if desired as well\n", " ),\n", - "]\n", - "# You can also define an args_schema to provide more information about inputs\n", + "]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e9b560f7", + "metadata": {}, + "source": [ + "You can also define a custom `args_schema`` to provide more information about inputs." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "631361e7", + "metadata": {}, + "outputs": [], + "source": [ "from pydantic import BaseModel, Field\n", "\n", "class CalculatorInput(BaseModel):\n", @@ -101,18 +133,19 @@ " \n", "\n", "tools.append(\n", - " Tool(\n", - " name=\"Calculator\",\n", + " Tool.from_function(\n", " func=llm_math_chain.run,\n", + " name=\"Calculator\",\n", " description=\"useful for when you need to answer questions about math\",\n", " args_schema=CalculatorInput\n", + " # coroutine= ... <- you can specify an async method if desired as well\n", " )\n", ")" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "5b93047d", "metadata": { "tags": [] @@ -126,7 +159,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "6f96a891", "metadata": { "tags": [] @@ -141,7 +174,17 @@ "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", "\u001b[32;1m\u001b[1;3mI need to find out Leo DiCaprio's girlfriend's name and her age\n", "Action: Search\n", - "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\u001b[36;1m\u001b[1;3mDiCaprio broke up with girlfriend Camila Morrone, 25, in the summer of 2022, after dating for four years.\u001b[0m\u001b[32;1m\u001b[1;3mI need to find out Camila Morrone's current age\n", + "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mAfter rumours of a romance with Gigi Hadid, the Oscar winner has seemingly moved on. First being linked to the television personality in September 2022, it appears as if his \"age bracket\" has moved up. This follows his rumoured relationship with mere 19-year-old Eden Polani.\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI still need to find out his current girlfriend's name and age\n", + "Action: Search\n", + "Action Input: \"Leo DiCaprio current girlfriend\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mJust Jared on Instagram: “Leonardo DiCaprio & girlfriend Camila Morrone couple up for a lunch date!\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mNow that I know his girlfriend's name is Camila Morrone, I need to find her current age\n", + "Action: Search\n", + "Action Input: \"Camila Morrone age\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3m25 years\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mNow that I have her age, I need to calculate her age raised to the 0.43 power\n", "Action: Calculator\n", "Action Input: 25^(0.43)\u001b[0m\n", "\n", @@ -153,8 +196,10 @@ "\u001b[0m\n", "Answer: \u001b[33;1m\u001b[1;3m3.991298452658078\u001b[0m\n", "\u001b[1m> Finished chain.\u001b[0m\n", - "\u001b[33;1m\u001b[1;3mAnswer: 3.991298452658078\u001b[0m\u001b[32;1m\u001b[1;3mI now know the final answer\n", - "Final Answer: 3.991298452658078\u001b[0m\n", + "\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 3.991298452658078\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI now know the final answer\n", + "Final Answer: Camila Morrone's current age raised to the 0.43 power is approximately 3.99.\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n" ] @@ -162,10 +207,10 @@ { "data": { "text/plain": [ - "'3.991298452658078'" + "\"Camila Morrone's current age raised to the 0.43 power is approximately 3.99.\"" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -175,71 +220,65 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "id": "6f12eaf0", "metadata": {}, "source": [ - "### Subclassing the BaseTool class" + "### Subclassing the BaseTool class\n", + "\n", + "You can also directly subclass `BaseTool`. This is useful if you want more control over the instance variables or if you want to propagate callbacks to nested chains or other tools." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "c58a7c40", "metadata": { "tags": [] }, "outputs": [], "source": [ - "from typing import Type\n", + "from typing import Optional, Type\n", + "\n", + "from langchain.callbacks.manager import AsyncCallbackManagerForToolRun, CallbackManagerForToolRun\n", "\n", "class CustomSearchTool(BaseTool):\n", - " name = \"Search\"\n", + " name = \"custom_search\"\n", " description = \"useful for when you need to answer questions about current events\"\n", "\n", - " def _run(self, query: str) -> str:\n", + " def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:\n", " \"\"\"Use the tool.\"\"\"\n", " return search.run(query)\n", " \n", - " async def _arun(self, query: str) -> str:\n", + " async def _arun(self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:\n", " \"\"\"Use the tool asynchronously.\"\"\"\n", - " raise NotImplementedError(\"BingSearchRun does not support async\")\n", + " raise NotImplementedError(\"custom_search does not support async\")\n", " \n", "class CustomCalculatorTool(BaseTool):\n", " name = \"Calculator\"\n", " description = \"useful for when you need to answer questions about math\"\n", " args_schema: Type[BaseModel] = CalculatorInput\n", "\n", - " def _run(self, query: str) -> str:\n", + " def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:\n", " \"\"\"Use the tool.\"\"\"\n", " return llm_math_chain.run(query)\n", " \n", - " async def _arun(self, query: str) -> str:\n", + " async def _arun(self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:\n", " \"\"\"Use the tool asynchronously.\"\"\"\n", - " raise NotImplementedError(\"BingSearchRun does not support async\")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "3318a46f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "tools = [CustomSearchTool(), CustomCalculatorTool()]" + " raise NotImplementedError(\"Calculator does not support async\")" ] }, { "cell_type": "code", "execution_count": 8, - "id": "ee2d0f3a", + "id": "3318a46f", "metadata": { "tags": [] }, "outputs": [], "source": [ + "tools = [CustomSearchTool(), CustomCalculatorTool()]\n", "agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)" ] }, @@ -258,22 +297,30 @@ "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3mI need to find out Leo DiCaprio's girlfriend's name and her age\n", - "Action: Search\n", - "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\u001b[36;1m\u001b[1;3mDiCaprio broke up with girlfriend Camila Morrone, 25, in the summer of 2022, after dating for four years.\u001b[0m\u001b[32;1m\u001b[1;3mI need to find out Camila Morrone's current age\n", + "\u001b[32;1m\u001b[1;3mI need to use custom_search to find out who Leo DiCaprio's girlfriend is, and then use the Calculator to raise her age to the 0.43 power.\n", + "Action: custom_search\n", + "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mAfter rumours of a romance with Gigi Hadid, the Oscar winner has seemingly moved on. First being linked to the television personality in September 2022, it appears as if his \"age bracket\" has moved up. This follows his rumoured relationship with mere 19-year-old Eden Polani.\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI need to find out the current age of Eden Polani.\n", + "Action: custom_search\n", + "Action Input: \"Eden Polani age\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3m19 years old\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mNow I can use the Calculator to raise her age to the 0.43 power.\n", "Action: Calculator\n", - "Action Input: 25^(0.43)\u001b[0m\n", + "Action Input: 19 ^ 0.43\u001b[0m\n", "\n", "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", - "25^(0.43)\u001b[32;1m\u001b[1;3m```text\n", - "25**(0.43)\n", + "19 ^ 0.43\u001b[32;1m\u001b[1;3m```text\n", + "19 ** 0.43\n", "```\n", - "...numexpr.evaluate(\"25**(0.43)\")...\n", + "...numexpr.evaluate(\"19 ** 0.43\")...\n", "\u001b[0m\n", - "Answer: \u001b[33;1m\u001b[1;3m3.991298452658078\u001b[0m\n", + "Answer: \u001b[33;1m\u001b[1;3m3.547023357958959\u001b[0m\n", "\u001b[1m> Finished chain.\u001b[0m\n", - "\u001b[33;1m\u001b[1;3mAnswer: 3.991298452658078\u001b[0m\u001b[32;1m\u001b[1;3mI now know the final answer\n", - "Final Answer: 3.991298452658078\u001b[0m\n", + "\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 3.547023357958959\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI now know the final answer.\n", + "Final Answer: 3.547023357958959\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n" ] @@ -281,7 +328,7 @@ { "data": { "text/plain": [ - "'3.991298452658078'" + "'3.547023357958959'" ] }, "execution_count": 9, @@ -312,34 +359,13 @@ }, "outputs": [], "source": [ - "from langchain.agents import tool\n", + "from langchain.tools import tool\n", "\n", "@tool\n", "def search_api(query: str) -> str:\n", " \"\"\"Searches the API for the query.\"\"\"\n", - " return f\"Results for query {query}\"" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "0a23b91b", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "Tool(name='search_api', description='search_api(query: str) -> str - Searches the API for the query.', args_schema=, return_direct=False, verbose=False, callback_manager=, func=, coroutine=None)" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + " return f\"Results for query {query}\"\n", + "\n", "search_api" ] }, @@ -433,18 +459,149 @@ ] }, { + "attachments": {}, + "cell_type": "markdown", + "id": "61d2e80b", + "metadata": {}, + "source": [ + "## Custom Structured Tools\n", + "\n", + "If your functions require more structured arguments, you can use the `StructuredTool` class directly, or still subclass the `BaseTool` class." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "5be41722", + "metadata": {}, + "source": [ + "### StructuredTool dataclass\n", + "\n", + "To dynamically generate a structured tool from a given function, the fastest way to get started is with `StructuredTool.from_function()`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3c070216", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from langchain.tools import StructuredTool\n", + "\n", + "def post_message(url: str, body: dict, parameters: Optional[dict] = None) -> str:\n", + " \"\"\"Sends a POST request to the given url with the given body and parameters.\"\"\"\n", + " result = requests.post(url, json=body, params=parameters)\n", + " return f\"Status: {result.status_code} - {result.text}\"\n", + "\n", + "tool = StructuredTool.from_function(post_message)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "fb0a38eb", + "metadata": {}, + "source": [ + "## Subclassing the BaseTool\n", + "\n", + "The BaseTool automatically infers the schema from the _run method's signature." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7505c9c5", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Optional, Type\n", + "\n", + "from langchain.callbacks.manager import AsyncCallbackManagerForToolRun, CallbackManagerForToolRun\n", + " \n", + "class CustomSearchTool(BaseTool):\n", + " name = \"custom_search\"\n", + " description = \"useful for when you need to answer questions about current events\"\n", + "\n", + " def _run(self, query: str, engine: str = \"google\", gl: str = \"us\", hl: str = \"en\", run_manager: Optional[CallbackManagerForToolRun] = None) -> str:\n", + " \"\"\"Use the tool.\"\"\"\n", + " search_wrapper = SerpAPIWrapper(params={\"engine\": engine, \"gl\": gl, \"hl\": hl})\n", + " return search_wrapper.run(query)\n", + " \n", + " async def _arun(self, query: str, engine: str = \"google\", gl: str = \"us\", hl: str = \"en\", run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:\n", + " \"\"\"Use the tool asynchronously.\"\"\"\n", + " raise NotImplementedError(\"custom_search does not support async\")\n", + "\n", + "\n", + "\n", + "# You can provide a custom args schema to add descriptions or custom validation\n", + "\n", + "class SearchSchema(BaseModel):\n", + " query: str = Field(description=\"should be a search query\")\n", + " engine: str = Field(description=\"should be a search engine\")\n", + " gl: str = Field(description=\"should be a country code\")\n", + " hl: str = Field(description=\"should be a language code\")\n", + "\n", + "class CustomSearchTool(BaseTool):\n", + " name = \"custom_search\"\n", + " description = \"useful for when you need to answer questions about current events\"\n", + " args_schema: Type[SearchSchema] = SearchSchema\n", + "\n", + " def _run(self, query: str, engine: str = \"google\", gl: str = \"us\", hl: str = \"en\", run_manager: Optional[CallbackManagerForToolRun] = None) -> str:\n", + " \"\"\"Use the tool.\"\"\"\n", + " search_wrapper = SerpAPIWrapper(params={\"engine\": engine, \"gl\": gl, \"hl\": hl})\n", + " return search_wrapper.run(query)\n", + " \n", + " async def _arun(self, query: str, engine: str = \"google\", gl: str = \"us\", hl: str = \"en\", run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:\n", + " \"\"\"Use the tool asynchronously.\"\"\"\n", + " raise NotImplementedError(\"custom_search does not support async\")\n", + " \n", + " " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7d68b0ac", + "metadata": {}, + "source": [ + "## Using the decorator\n", + "\n", + "The `tool` decorator creates a structured tool automatically if the signature has multiple arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "38d11416", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from langchain.tools import tool\n", + "\n", + "@tool\n", + "def post_message(url: str, body: dict, parameters: Optional[dict] = None) -> str:\n", + " \"\"\"Sends a POST request to the given url with the given body and parameters.\"\"\"\n", + " result = requests.post(url, json=body, params=parameters)\n", + " return f\"Status: {result.status_code} - {result.text}\"" + ] + }, + { + "attachments": {}, "cell_type": "markdown", "id": "1d0430d6", "metadata": {}, "source": [ "## Modify existing tools\n", "\n", - "Now, we show how to load existing tools and just modify them. In the example below, we do something really simple and change the Search tool to have the name `Google Search`." + "Now, we show how to load existing tools and modify them directly. In the example below, we do something really simple and change the Search tool to have the name `Google Search`." ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "id": "79213f40", "metadata": {}, "outputs": [], @@ -454,7 +611,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "id": "e1067dcb", "metadata": {}, "outputs": [], @@ -464,7 +621,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "id": "6c66ffe8", "metadata": {}, "outputs": [], @@ -474,7 +631,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "id": "f45b5bc3", "metadata": {}, "outputs": [], @@ -484,7 +641,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "id": "565e2b9b", "metadata": {}, "outputs": [ @@ -497,10 +654,18 @@ "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", "\u001b[32;1m\u001b[1;3mI need to find out Leo DiCaprio's girlfriend's name and her age.\n", "Action: Google Search\n", - "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\u001b[36;1m\u001b[1;3mI draw the lime at going to get a Mohawk, though.\" DiCaprio broke up with girlfriend Camila Morrone, 25, in the summer of 2022, after dating for four years. He's since been linked to another famous supermodel – Gigi Hadid.\u001b[0m\u001b[32;1m\u001b[1;3mNow I need to find out Camila Morrone's current age.\n", + "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mAfter rumours of a romance with Gigi Hadid, the Oscar winner has seemingly moved on. First being linked to the television personality in September 2022, it appears as if his \"age bracket\" has moved up. This follows his rumoured relationship with mere 19-year-old Eden Polani.\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI still need to find out his current girlfriend's name and her age.\n", + "Action: Google Search\n", + "Action Input: \"Leo DiCaprio current girlfriend age\"\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mLeonardo DiCaprio has been linked with 19-year-old model Eden Polani, continuing the rumour that he doesn't date any women over the age of ...\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI need to find out the age of Eden Polani.\n", "Action: Calculator\n", - "Action Input: 25^0.43\u001b[0m\u001b[33;1m\u001b[1;3mAnswer: 3.991298452658078\u001b[0m\u001b[32;1m\u001b[1;3mI now know the final answer.\n", - "Final Answer: Camila Morrone's current age raised to the 0.43 power is approximately 3.99.\u001b[0m\n", + "Action Input: 19^(0.43)\u001b[0m\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 3.547023357958959\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mI now know the final answer.\n", + "Final Answer: The age of Leo DiCaprio's girlfriend raised to the 0.43 power is approximately 3.55.\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n" ] @@ -508,10 +673,10 @@ { "data": { "text/plain": [ - "\"Camila Morrone's current age raised to the 0.43 power is approximately 3.99.\"" + "\"The age of Leo DiCaprio's girlfriend raised to the 0.43 power is approximately 3.55.\"" ] }, - "execution_count": 18, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -537,7 +702,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "id": "3450512e", "metadata": {}, "outputs": [], diff --git a/docs/modules/agents/tools/examples/google_search.ipynb b/docs/modules/agents/tools/examples/google_search.ipynb index 2d664664..84c2a351 100644 --- a/docs/modules/agents/tools/examples/google_search.ipynb +++ b/docs/modules/agents/tools/examples/google_search.ipynb @@ -33,7 +33,16 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.utilities import GoogleSearchAPIWrapper" + "from langchain.tools import Tool\n", + "from langchain.utilities import GoogleSearchAPIWrapper\n", + "\n", + "search = GoogleSearchAPIWrapper()\n", + "\n", + "tool = Tool(\n", + " name = \"Google Search\",\n", + " description=\"Search Google for recent results.\",\n", + " func=search.run\n", + ")" ] }, { @@ -41,30 +50,20 @@ "execution_count": 3, "id": "84b8f773", "metadata": {}, - "outputs": [], - "source": [ - "search = GoogleSearchAPIWrapper()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "068991a6", - "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'1 Child\\'s First Name. 2. 6. 7d. Street Address. 71. (Type or print). BARACK. Sex. 3. This Birth. 4. If Twin or Triplet,. Was Child Born. Barack Hussein Obama II is an American retired politician who served as the 44th president of the United States from 2009 to 2017. His full name is Barack Hussein Obama II. Since the “II” is simply because he was named for his father, his last name is Obama. Feb 9, 2015 ... Michael Jordan misspelled Barack Obama\\'s first name on 50th-birthday gift ... Knowing Obama is a Chicagoan and huge basketball fan,\\xa0... Aug 18, 2017 ... It took him several seconds and multiple clues to remember former President Barack Obama\\'s first name. Miller knew that every answer had to end\\xa0... First Lady Michelle LaVaughn Robinson Obama is a lawyer, writer, and the wife of the 44th President, Barack Obama. She is the first African-American First\\xa0... Barack Obama, in full Barack Hussein Obama II, (born August 4, 1961, Honolulu, Hawaii, U.S.), 44th president of the United States (2009–17) and the first\\xa0... When Barack Obama was elected president in 2008, he became the first African American to hold ... The Middle East remained a key foreign policy challenge. Feb 27, 2020 ... President Barack Obama was born Barack Hussein Obama, II, as shown here on his birth certificate here . As reported by Reuters here , his\\xa0... Jan 16, 2007 ... 4, 1961, in Honolulu. His first name means \"one who is blessed\" in Swahili. While Obama\\'s father, Barack Hussein Obama Sr., was from Kenya, his\\xa0...'" + "\"STATE OF HAWAII. 1 Child's First Name. (Type or print). 2. Sex. BARACK. 3. This Birth. CERTIFICATE OF LIVE BIRTH. FILE. NUMBER 151 le. lb. Middle Name. Barack Hussein Obama II is an American former politician who served as the 44th president of the United States from 2009 to 2017. A member of the Democratic\\xa0... When Barack Obama was elected president in 2008, he became the first African American to hold ... The Middle East remained a key foreign policy challenge. Jan 19, 2017 ... Jordan Barack Treasure, New York City, born in 2008 ... Jordan Barack Treasure made national news when he was the focus of a New York newspaper\\xa0... Portrait of George Washington, the 1st President of the United States ... Portrait of Barack Obama, the 44th President of the United States\\xa0... His full name is Barack Hussein Obama II. Since the “II” is simply because he was named for his father, his last name is Obama. Mar 22, 2008 ... Barry Obama decided that he didn't like his nickname. A few of his friends at Occidental College had already begun to call him Barack (his\\xa0... Aug 18, 2017 ... It took him several seconds and multiple clues to remember former President Barack Obama's first name. Miller knew that every answer had to\\xa0... Feb 9, 2015 ... Michael Jordan misspelled Barack Obama's first name on 50th-birthday gift ... Knowing Obama is a Chicagoan and huge basketball fan,\\xa0... 4 days ago ... Barack Obama, in full Barack Hussein Obama II, (born August 4, 1961, Honolulu, Hawaii, U.S.), 44th president of the United States (2009–17) and\\xa0...\"" ] }, - "execution_count": 4, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "search.run(\"Obama's first name?\")" + "tool.run(\"Obama's first name?\")" ] }, { @@ -78,17 +77,23 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "5083fbdd", "metadata": {}, "outputs": [], "source": [ - "search = GoogleSearchAPIWrapper(k=1)" + "search = GoogleSearchAPIWrapper(k=1)\n", + "\n", + "tool = Tool(\n", + " name = \"I'm Feeling Lucky\",\n", + " description=\"Search Google and return the first result.\",\n", + " func=search.run\n", + ")" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "77aaa857", "metadata": {}, "outputs": [ @@ -98,13 +103,13 @@ "'The official home of the Python Programming Language.'" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "search.run(\"python\")" + "tool.run(\"python\")" ] }, { @@ -137,48 +142,30 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "028f4cba", "metadata": {}, "outputs": [], "source": [ - "search = GoogleSearchAPIWrapper()" + "search = GoogleSearchAPIWrapper()\n", + "\n", + "def top5_results(query):\n", + " return search.results(query, 5)\n", + "\n", + "tool = Tool(\n", + " name = \"Google Search Snippets\",\n", + " description=\"Search Google for recent results.\",\n", + " func=top5_results\n", + ")" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "4d8f734f", + "execution_count": null, + "id": "4d7f92e1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'snippet': 'Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment,\\xa0...',\n", - " 'title': 'Apple',\n", - " 'link': 'https://www.apple.com/'},\n", - " {'snippet': \"Jul 10, 2022 ... Whether or not you're up on your apple trivia, no doubt you know how delicious this popular fruit is, and how nutritious. Apples are rich in\\xa0...\",\n", - " 'title': '25 Types of Apples and What to Make With Them - Parade ...',\n", - " 'link': 'https://parade.com/1330308/bethlipton/types-of-apples/'},\n", - " {'snippet': 'An apple is an edible fruit produced by an apple tree (Malus domestica). Apple trees are cultivated worldwide and are the most widely grown species in the\\xa0...',\n", - " 'title': 'Apple - Wikipedia',\n", - " 'link': 'https://en.wikipedia.org/wiki/Apple'},\n", - " {'snippet': 'Apples are a popular fruit. They contain antioxidants, vitamins, dietary fiber, and a range of other nutrients. Due to their varied nutrient content,\\xa0...',\n", - " 'title': 'Apples: Benefits, nutrition, and tips',\n", - " 'link': 'https://www.medicalnewstoday.com/articles/267290'},\n", - " {'snippet': \"An apple is a crunchy, bright-colored fruit, one of the most popular in the United States. You've probably heard the age-old saying, “An apple a day keeps\\xa0...\",\n", - " 'title': 'Apples: Nutrition & Health Benefits',\n", - " 'link': 'https://www.webmd.com/food-recipes/benefits-apples'}]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "search.results(\"apples\", 5)" - ] + "outputs": [], + "source": [] } ], "metadata": { @@ -197,7 +184,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.2" }, "vscode": { "interpreter": { diff --git a/docs/modules/agents/tools/examples/requests.ipynb b/docs/modules/agents/tools/examples/requests.ipynb index a2b04e62..677c07dd 100644 --- a/docs/modules/agents/tools/examples/requests.ipynb +++ b/docs/modules/agents/tools/examples/requests.ipynb @@ -13,33 +13,94 @@ { "cell_type": "code", "execution_count": 3, - "id": "81aae09e", + "id": "5d8764ba", "metadata": {}, "outputs": [], "source": [ - "from langchain.utilities import TextRequestsWrapper" + "from langchain.agents import load_tools\n", + "\n", + "requests_tools = load_tools([\"requests_all\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "bc5edde2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[RequestsGetTool(name='requests_get', description='A portal to the internet. Use this when you need to get specific content from a website. Input should be a url (i.e. https://www.google.com). The output will be the text response of the GET request.', args_schema=None, return_direct=False, verbose=False, callbacks=None, callback_manager=None, requests_wrapper=TextRequestsWrapper(headers=None, aiosession=None)),\n", + " RequestsPostTool(name='requests_post', description='Use this when you want to POST to a website.\\n Input should be a json string with two keys: \"url\" and \"data\".\\n The value of \"url\" should be a string, and the value of \"data\" should be a dictionary of \\n key-value pairs you want to POST to the url.\\n Be careful to always use double quotes for strings in the json string\\n The output will be the text response of the POST request.\\n ', args_schema=None, return_direct=False, verbose=False, callbacks=None, callback_manager=None, requests_wrapper=TextRequestsWrapper(headers=None, aiosession=None)),\n", + " RequestsPatchTool(name='requests_patch', description='Use this when you want to PATCH to a website.\\n Input should be a json string with two keys: \"url\" and \"data\".\\n The value of \"url\" should be a string, and the value of \"data\" should be a dictionary of \\n key-value pairs you want to PATCH to the url.\\n Be careful to always use double quotes for strings in the json string\\n The output will be the text response of the PATCH request.\\n ', args_schema=None, return_direct=False, verbose=False, callbacks=None, callback_manager=None, requests_wrapper=TextRequestsWrapper(headers=None, aiosession=None)),\n", + " RequestsPutTool(name='requests_put', description='Use this when you want to PUT to a website.\\n Input should be a json string with two keys: \"url\" and \"data\".\\n The value of \"url\" should be a string, and the value of \"data\" should be a dictionary of \\n key-value pairs you want to PUT to the url.\\n Be careful to always use double quotes for strings in the json string.\\n The output will be the text response of the PUT request.\\n ', args_schema=None, return_direct=False, verbose=False, callbacks=None, callback_manager=None, requests_wrapper=TextRequestsWrapper(headers=None, aiosession=None)),\n", + " RequestsDeleteTool(name='requests_delete', description='A portal to the internet. Use this when you need to make a DELETE request to a URL. Input should be a specific url, and the output will be the text response of the DELETE request.', args_schema=None, return_direct=False, verbose=False, callbacks=None, callback_manager=None, requests_wrapper=TextRequestsWrapper(headers=None, aiosession=None))]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "requests_tools" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "55cfe672", + "metadata": {}, + "source": [ + "### Inside the tool\n", + "\n", + "Each requests tool contains a `requests` wrapper. You can work with these wrappers directly below" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c56d4678", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "TextRequestsWrapper(headers=None, aiosession=None)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Each tool wrapps a requests wrapper\n", + "requests_tools[0].requests_wrapper" ] }, { "cell_type": "code", "execution_count": 4, - "id": "fd210142", + "id": "81aae09e", "metadata": {}, "outputs": [], "source": [ - "requests = TextRequestsWrapper()" + "from langchain.utilities import TextRequestsWrapper\n", + "requests = TextRequestsWrapper()\n" ] }, { "cell_type": "code", "execution_count": 5, - "id": "29a77bb2", + "id": "fd210142", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Google

\"International

 

Advanced search

Celebrate International Women\\'s Day with Google

© 2023 - Privacy - Terms

'" + "'Google

\"Google\"

 

Advanced search

© 2023 - Privacy - Terms

'" ] }, "execution_count": 5, @@ -76,7 +137,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/docs/modules/agents/tools/examples/sceneXplain.ipynb b/docs/modules/agents/tools/examples/sceneXplain.ipynb index 48ec6402..41b57df4 100644 --- a/docs/modules/agents/tools/examples/sceneXplain.ipynb +++ b/docs/modules/agents/tools/examples/sceneXplain.ipynb @@ -20,10 +20,37 @@ "outputs": [], "source": [ "import os\n", + "os.environ[\"SCENEX_API_KEY\"] = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents import load_tools\n", + "\n", + "tools = load_tools([\"sceneXplain\"])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or directly instantiate the tool." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ "from langchain.tools import SceneXplainTool\n", "\n", "\n", - "os.environ[\"SCENEX_API_KEY\"] = \"\"\n", "tool = SceneXplainTool()\n" ] }, @@ -73,10 +100,6 @@ "\n", "llm = OpenAI(temperature=0)\n", "memory = ConversationBufferMemory(memory_key=\"chat_history\")\n", - "tools = [\n", - " tool\n", - "]\n", - "\n", "agent = initialize_agent(\n", " tools, llm, memory=memory, agent=\"conversational-react-description\", verbose=True\n", ")\n", diff --git a/langchain/agents/__init__.py b/langchain/agents/__init__.py index a49e5974..74ddb5ca 100644 --- a/langchain/agents/__init__.py +++ b/langchain/agents/__init__.py @@ -30,33 +30,33 @@ from langchain.agents.self_ask_with_search.base import SelfAskWithSearchChain from langchain.agents.tools import Tool, tool __all__ = [ + "Agent", + "AgentExecutor", + "AgentOutputParser", + "AgentType", + "BaseMultiActionAgent", + "BaseSingleActionAgent", + "ConversationalAgent", + "ConversationalChatAgent", + "LLMSingleActionAgent", "MRKLChain", - "SelfAskWithSearchChain", "ReActChain", - "AgentExecutor", - "Agent", + "ReActTextWorldAgent", + "SelfAskWithSearchChain", "Tool", - "tool", - "initialize_agent", "ZeroShotAgent", - "ReActTextWorldAgent", - "load_tools", - "get_all_tool_names", - "ConversationalAgent", - "ConversationalChatAgent", - "load_agent", - "create_sql_agent", - "create_pbi_agent", - "create_pbi_chat_agent", + "create_csv_agent", "create_json_agent", "create_openapi_agent", - "create_vectorstore_router_agent", - "create_vectorstore_agent", "create_pandas_dataframe_agent", - "create_csv_agent", - "LLMSingleActionAgent", - "AgentOutputParser", - "BaseSingleActionAgent", - "AgentType", - "BaseMultiActionAgent", + "create_pbi_agent", + "create_pbi_chat_agent", + "create_sql_agent", + "create_vectorstore_agent", + "create_vectorstore_router_agent", + "get_all_tool_names", + "initialize_agent", + "load_agent", + "load_tools", + "tool", ] diff --git a/langchain/agents/load_tools.py b/langchain/agents/load_tools.py index d858e960..be8bba2b 100644 --- a/langchain/agents/load_tools.py +++ b/langchain/agents/load_tools.py @@ -26,6 +26,7 @@ from langchain.tools.requests.tool import ( RequestsPostTool, RequestsPutTool, ) +from langchain.tools.scenexplain.tool import SceneXplainTool from langchain.tools.searx_search.tool import SearxSearchResults, SearxSearchRun from langchain.tools.shell.tool import ShellTool from langchain.tools.wikipedia.tool import WikipediaQueryRun @@ -232,6 +233,10 @@ def _get_human_tool(**kwargs: Any) -> BaseTool: return HumanInputRun(**kwargs) +def _get_scenexplain(**kwargs: Any) -> BaseTool: + return SceneXplainTool(**kwargs) + + _EXTRA_LLM_TOOLS: Dict[ str, Tuple[Callable[[Arg(BaseLLM, "llm"), KwArg(Any)], BaseTool], List[str]] ] = { @@ -266,6 +271,7 @@ _EXTRA_OPTIONAL_TOOLS: Dict[str, Tuple[Callable[[KwArg(Any)], BaseTool], List[st _get_lambda_api, ["awslambda_tool_name", "awslambda_tool_description", "function_name"], ), + "sceneXplain": (_get_scenexplain, []), } diff --git a/langchain/agents/tools.py b/langchain/agents/tools.py index 0f943138..b7ac0c5d 100644 --- a/langchain/agents/tools.py +++ b/langchain/agents/tools.py @@ -1,102 +1,11 @@ """Interface for tools.""" -from functools import partial -from inspect import signature -from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, Type, Union - -from pydantic import BaseModel, validator +from typing import Optional from langchain.callbacks.manager import ( AsyncCallbackManagerForToolRun, CallbackManagerForToolRun, ) -from langchain.tools.base import BaseTool, StructuredTool - - -class Tool(BaseTool): - """Tool that takes in function or coroutine directly.""" - - description: str = "" - func: Callable[..., str] - """The function to run when the tool is called.""" - coroutine: Optional[Callable[..., Awaitable[str]]] = None - """The asynchronous version of the function.""" - - @validator("func", pre=True, always=True) - def validate_func_not_partial(cls, func: Callable) -> Callable: - """Check that the function is not a partial.""" - if isinstance(func, partial): - raise ValueError("Partial functions not yet supported in tools.") - return func - - @property - def args(self) -> dict: - """The tool's input arguments.""" - if self.args_schema is not None: - return self.args_schema.schema()["properties"] - # For backwards compatibility, if the function signature is ambiguous, - # assume it takes a single string input. - return {"tool_input": {"type": "string"}} - - def _to_args_and_kwargs(self, tool_input: Union[str, Dict]) -> Tuple[Tuple, Dict]: - """Convert tool input to pydantic model.""" - args, kwargs = super()._to_args_and_kwargs(tool_input) - # For backwards compatibility. The tool must be run with a single input - all_args = list(args) + list(kwargs.values()) - if len(all_args) != 1: - raise ValueError( - f"Too many arguments to single-input tool {self.name}." - f" Args: {all_args}" - ) - return tuple(all_args), {} - - def _run( - self, - *args: Any, - run_manager: Optional[CallbackManagerForToolRun] = None, - **kwargs: Any, - ) -> Any: - """Use the tool.""" - new_argument_supported = signature(self.func).parameters.get("callbacks") - return ( - self.func( - *args, - callbacks=run_manager.get_child() if run_manager else None, - **kwargs, - ) - if new_argument_supported - else self.func(*args, **kwargs) - ) - - async def _arun( - self, - *args: Any, - run_manager: Optional[AsyncCallbackManagerForToolRun] = None, - **kwargs: Any, - ) -> Any: - """Use the tool asynchronously.""" - if self.coroutine: - new_argument_supported = signature(self.coroutine).parameters.get( - "callbacks" - ) - return ( - await self.coroutine( - *args, - callbacks=run_manager.get_child() if run_manager else None, - **kwargs, - ) - if new_argument_supported - else await self.coroutine(*args, **kwargs) - ) - raise NotImplementedError("Tool does not support async") - - # TODO: this is for backwards compatibility, remove in future - def __init__( - self, name: str, func: Callable, description: str, **kwargs: Any - ) -> None: - """Initialize tool.""" - super(Tool, self).__init__( - name=name, func=func, description=description, **kwargs - ) +from langchain.tools.base import BaseTool, Tool, tool class InvalidTool(BaseTool): @@ -120,77 +29,4 @@ class InvalidTool(BaseTool): return f"{tool_name} is not a valid tool, try another one." -def tool( - *args: Union[str, Callable], - return_direct: bool = False, - args_schema: Optional[Type[BaseModel]] = None, - infer_schema: bool = True, -) -> Callable: - """Make tools out of functions, can be used with or without arguments. - - Args: - *args: The arguments to the tool. - return_direct: Whether to return directly from the tool rather - than continuing the agent loop. - args_schema: optional argument schema for user to specify - infer_schema: Whether to infer the schema of the arguments from - the function's signature. This also makes the resultant tool - accept a dictionary input to its `run()` function. - - Requires: - - Function must be of type (str) -> str - - Function must have a docstring - - Examples: - .. code-block:: python - - @tool - def search_api(query: str) -> str: - # Searches the API for the query. - return - - @tool("search", return_direct=True) - def search_api(query: str) -> str: - # Searches the API for the query. - return - """ - - def _make_with_name(tool_name: str) -> Callable: - def _make_tool(func: Callable) -> BaseTool: - if infer_schema or args_schema is not None: - return StructuredTool.from_function( - func, - name=tool_name, - return_direct=return_direct, - args_schema=args_schema, - infer_schema=infer_schema, - ) - # If someone doesn't want a schema applied, we must treat it as - # a simple string->string function - assert func.__doc__ is not None, "Function must have a docstring" - return Tool( - name=tool_name, - func=func, - description=f"{tool_name} tool", - return_direct=return_direct, - ) - - return _make_tool - - if len(args) == 1 and isinstance(args[0], str): - # if the argument is a string, then we use the string as the tool name - # Example usage: @tool("search", return_direct=True) - return _make_with_name(args[0]) - elif len(args) == 1 and callable(args[0]): - # if the argument is a function, then we use the function name as the tool name - # Example usage: @tool - return _make_with_name(args[0].__name__)(args[0]) - elif len(args) == 0: - # if there are no arguments, then we use the function name as the tool name - # Example usage: @tool(return_direct=True) - def _partial(func: Callable[[str], str]) -> BaseTool: - return _make_with_name(func.__name__)(func) - - return _partial - else: - raise ValueError("Too many arguments for tool decorator") +__all__ = ["InvalidTool", "BaseTool", "tool", "Tool"] diff --git a/langchain/tools/__init__.py b/langchain/tools/__init__.py index eb51a596..af95cfa2 100644 --- a/langchain/tools/__init__.py +++ b/langchain/tools/__init__.py @@ -1,6 +1,6 @@ """Core toolkit implementations.""" -from langchain.tools.base import BaseTool, StructuredTool +from langchain.tools.base import BaseTool, StructuredTool, Tool, tool from langchain.tools.bing_search.tool import BingSearchResults, BingSearchRun from langchain.tools.ddg_search.tool import DuckDuckGoSearchResults, DuckDuckGoSearchRun from langchain.tools.file_management.copy import CopyFileTool @@ -41,6 +41,7 @@ __all__ = [ "APIOperation", "BaseTool", "BaseTool", + "BaseTool", "BingSearchResults", "BingSearchRun", "ClickTool", @@ -49,7 +50,6 @@ __all__ = [ "DeleteFileTool", "DuckDuckGoSearchResults", "DuckDuckGoSearchRun", - "DuckDuckGoSearchRun", "ExtractHyperlinksTool", "ExtractTextTool", "FileSearchTool", @@ -65,15 +65,16 @@ __all__ = [ "NavigateTool", "OpenAPISpec", "ReadFileTool", + "SceneXplainTool", "ShellTool", "StructuredTool", - "WriteFileTool", - "BaseTool", - "SceneXplainTool", - "VectorStoreQAWithSourcesTool", + "Tool", "VectorStoreQATool", + "VectorStoreQAWithSourcesTool", "WikipediaQueryRun", "WolframAlphaQueryRun", - "ZapierNLARunAction", + "WriteFileTool", "ZapierNLAListActions", + "ZapierNLARunAction", + "tool", ] diff --git a/langchain/tools/base.py b/langchain/tools/base.py index 6799409e..f231b5ef 100644 --- a/langchain/tools/base.py +++ b/langchain/tools/base.py @@ -3,6 +3,7 @@ from __future__ import annotations import warnings from abc import ABC, abstractmethod +from functools import partial from inspect import signature from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, Type, Union @@ -13,6 +14,7 @@ from pydantic import ( create_model, root_validator, validate_arguments, + validator, ) from pydantic.main import ModelMetaclass @@ -298,6 +300,113 @@ class BaseTool(ABC, BaseModel, metaclass=ToolMetaclass): return self.run(tool_input, callbacks=callbacks) +class Tool(BaseTool): + """Tool that takes in function or coroutine directly.""" + + description: str = "" + func: Callable[..., str] + """The function to run when the tool is called.""" + coroutine: Optional[Callable[..., Awaitable[str]]] = None + """The asynchronous version of the function.""" + + @validator("func", pre=True, always=True) + def validate_func_not_partial(cls, func: Callable) -> Callable: + """Check that the function is not a partial.""" + if isinstance(func, partial): + raise ValueError("Partial functions not yet supported in tools.") + return func + + @property + def args(self) -> dict: + """The tool's input arguments.""" + if self.args_schema is not None: + return self.args_schema.schema()["properties"] + # For backwards compatibility, if the function signature is ambiguous, + # assume it takes a single string input. + return {"tool_input": {"type": "string"}} + + def _to_args_and_kwargs(self, tool_input: Union[str, Dict]) -> Tuple[Tuple, Dict]: + """Convert tool input to pydantic model.""" + args, kwargs = super()._to_args_and_kwargs(tool_input) + # For backwards compatibility. The tool must be run with a single input + all_args = list(args) + list(kwargs.values()) + if len(all_args) != 1: + raise ValueError( + f"Too many arguments to single-input tool {self.name}." + f" Args: {all_args}" + ) + return tuple(all_args), {} + + def _run( + self, + *args: Any, + run_manager: Optional[CallbackManagerForToolRun] = None, + **kwargs: Any, + ) -> Any: + """Use the tool.""" + new_argument_supported = signature(self.func).parameters.get("callbacks") + return ( + self.func( + *args, + callbacks=run_manager.get_child() if run_manager else None, + **kwargs, + ) + if new_argument_supported + else self.func(*args, **kwargs) + ) + + async def _arun( + self, + *args: Any, + run_manager: Optional[AsyncCallbackManagerForToolRun] = None, + **kwargs: Any, + ) -> Any: + """Use the tool asynchronously.""" + if self.coroutine: + new_argument_supported = signature(self.coroutine).parameters.get( + "callbacks" + ) + return ( + await self.coroutine( + *args, + callbacks=run_manager.get_child() if run_manager else None, + **kwargs, + ) + if new_argument_supported + else await self.coroutine(*args, **kwargs) + ) + raise NotImplementedError("Tool does not support async") + + # TODO: this is for backwards compatibility, remove in future + def __init__( + self, name: str, func: Callable, description: str, **kwargs: Any + ) -> None: + """Initialize tool.""" + super(Tool, self).__init__( + name=name, func=func, description=description, **kwargs + ) + + @classmethod + def from_function( + cls, + func: Callable, + name: str, # We keep these required to support backwards compatibility + description: str, + return_direct: bool = False, + args_schema: Optional[Type[BaseModel]] = None, + **kwargs: Any, + ) -> Tool: + """Initialize tool from a function.""" + return cls( + name=name, + func=func, + description=description, + return_direct=return_direct, + args_schema=args_schema, + **kwargs, + ) + + class StructuredTool(BaseTool): """Tool that can operate on any number of inputs.""" @@ -385,3 +494,79 @@ class StructuredTool(BaseTool): return_direct=return_direct, **kwargs, ) + + +def tool( + *args: Union[str, Callable], + return_direct: bool = False, + args_schema: Optional[Type[BaseModel]] = None, + infer_schema: bool = True, +) -> Callable: + """Make tools out of functions, can be used with or without arguments. + + Args: + *args: The arguments to the tool. + return_direct: Whether to return directly from the tool rather + than continuing the agent loop. + args_schema: optional argument schema for user to specify + infer_schema: Whether to infer the schema of the arguments from + the function's signature. This also makes the resultant tool + accept a dictionary input to its `run()` function. + + Requires: + - Function must be of type (str) -> str + - Function must have a docstring + + Examples: + .. code-block:: python + + @tool + def search_api(query: str) -> str: + # Searches the API for the query. + return + + @tool("search", return_direct=True) + def search_api(query: str) -> str: + # Searches the API for the query. + return + """ + + def _make_with_name(tool_name: str) -> Callable: + def _make_tool(func: Callable) -> BaseTool: + if infer_schema or args_schema is not None: + return StructuredTool.from_function( + func, + name=tool_name, + return_direct=return_direct, + args_schema=args_schema, + infer_schema=infer_schema, + ) + # If someone doesn't want a schema applied, we must treat it as + # a simple string->string function + assert func.__doc__ is not None, "Function must have a docstring" + return Tool( + name=tool_name, + func=func, + description=f"{tool_name} tool", + return_direct=return_direct, + ) + + return _make_tool + + if len(args) == 1 and isinstance(args[0], str): + # if the argument is a string, then we use the string as the tool name + # Example usage: @tool("search", return_direct=True) + return _make_with_name(args[0]) + elif len(args) == 1 and callable(args[0]): + # if the argument is a function, then we use the function name as the tool name + # Example usage: @tool + return _make_with_name(args[0].__name__)(args[0]) + elif len(args) == 0: + # if there are no arguments, then we use the function name as the tool name + # Example usage: @tool(return_direct=True) + def _partial(func: Callable[[str], str]) -> BaseTool: + return _make_with_name(func.__name__)(func) + + return _partial + else: + raise ValueError("Too many arguments for tool decorator") diff --git a/langchain/tools/plugin.py b/langchain/tools/plugin.py index 3d38895b..f452890b 100644 --- a/langchain/tools/plugin.py +++ b/langchain/tools/plugin.py @@ -49,7 +49,7 @@ def marshal_spec(txt: str) -> dict: return yaml.safe_load(txt) -class AIPLuginToolSchema(BaseModel): +class AIPluginToolSchema(BaseModel): """AIPLuginToolSchema.""" tool_input: Optional[str] = "" @@ -58,7 +58,7 @@ class AIPLuginToolSchema(BaseModel): class AIPluginTool(BaseTool): plugin: AIPlugin api_spec: str - args_schema: Type[AIPLuginToolSchema] = AIPLuginToolSchema + args_schema: Type[AIPluginToolSchema] = AIPluginToolSchema @classmethod def from_plugin_url(cls, url: str) -> AIPluginTool: diff --git a/langchain/utilities/scenexplain.py b/langchain/utilities/scenexplain.py index 7b3342c1..f5b8adf1 100644 --- a/langchain/utilities/scenexplain.py +++ b/langchain/utilities/scenexplain.py @@ -8,12 +8,12 @@ You can obtain a key by following the steps below. from typing import Dict import requests -from pydantic import BaseModel, root_validator +from pydantic import BaseModel, BaseSettings, Field, root_validator from langchain.utils import get_from_dict_or_env -class SceneXplainAPIWrapper(BaseModel): +class SceneXplainAPIWrapper(BaseSettings, BaseModel): """Wrapper for SceneXplain API. In order to set this up, you need API key for the SceneXplain API. @@ -23,7 +23,7 @@ class SceneXplainAPIWrapper(BaseModel): and create a new API key. """ - scenex_api_key: str + scenex_api_key: str = Field(..., env="SCENEX_API_KEY") scenex_api_url: str = ( "https://us-central1-causal-diffusion.cloudfunctions.net/describe" ) diff --git a/tests/unit_tests/agents/test_public_api.py b/tests/unit_tests/agents/test_public_api.py new file mode 100644 index 00000000..d7cda0d9 --- /dev/null +++ b/tests/unit_tests/agents/test_public_api.py @@ -0,0 +1,38 @@ +from langchain.agents import __all__ as agents_all + +_EXPECTED = [ + "Agent", + "AgentExecutor", + "AgentOutputParser", + "AgentType", + "BaseMultiActionAgent", + "BaseSingleActionAgent", + "ConversationalAgent", + "ConversationalChatAgent", + "LLMSingleActionAgent", + "MRKLChain", + "ReActChain", + "ReActTextWorldAgent", + "SelfAskWithSearchChain", + "Tool", + "ZeroShotAgent", + "create_csv_agent", + "create_json_agent", + "create_openapi_agent", + "create_pandas_dataframe_agent", + "create_pbi_agent", + "create_pbi_chat_agent", + "create_sql_agent", + "create_vectorstore_agent", + "create_vectorstore_router_agent", + "get_all_tool_names", + "initialize_agent", + "load_agent", + "load_tools", + "tool", +] + + +def test_public_api() -> None: + """Test for regressions or changes in the agents public API.""" + assert agents_all == sorted(_EXPECTED) diff --git a/tests/unit_tests/tools/test_exported.py b/tests/unit_tests/tools/test_exported.py new file mode 100644 index 00000000..6d90a76d --- /dev/null +++ b/tests/unit_tests/tools/test_exported.py @@ -0,0 +1,35 @@ +from typing import List, Type + +import langchain.tools +from langchain.tools import __all__ as tools_all +from langchain.tools.base import BaseTool, StructuredTool + +_EXCLUDE = { + BaseTool, + StructuredTool, +} + + +def _get_tool_classes(skip_tools_without_default_names: bool) -> List[Type[BaseTool]]: + results = [] + for tool_class_name in tools_all: + # Resolve the str to the class + tool_class = getattr(langchain.tools, tool_class_name) + if isinstance(tool_class, type) and issubclass(tool_class, BaseTool): + if tool_class in _EXCLUDE: + continue + if ( + skip_tools_without_default_names + and tool_class.__fields__["name"].default is None + ): + continue + results.append(tool_class) + return results + + +def test_tool_names_unique() -> None: + """Test that the default names for our core tools are unique.""" + tool_classes = _get_tool_classes(skip_tools_without_default_names=True) + names = sorted([tool_cls.__fields__["name"].default for tool_cls in tool_classes]) + duplicated_names = [name for name in names if names.count(name) > 1] + assert not duplicated_names diff --git a/tests/unit_tests/tools/test_public_api.py b/tests/unit_tests/tools/test_public_api.py new file mode 100644 index 00000000..792bac4a --- /dev/null +++ b/tests/unit_tests/tools/test_public_api.py @@ -0,0 +1,51 @@ +"""Test the public API of the tools package.""" +from langchain.tools import __all__ as public_api + +_EXPECTED = [ + "AIPluginTool", + "APIOperation", + "BaseTool", + "BaseTool", + "BaseTool", + "BingSearchResults", + "BingSearchRun", + "ClickTool", + "CopyFileTool", + "CurrentWebPageTool", + "DeleteFileTool", + "DuckDuckGoSearchResults", + "DuckDuckGoSearchRun", + "ExtractHyperlinksTool", + "ExtractTextTool", + "FileSearchTool", + "GetElementsTool", + "GooglePlacesTool", + "GoogleSearchResults", + "GoogleSearchRun", + "HumanInputRun", + "IFTTTWebhook", + "ListDirectoryTool", + "MoveFileTool", + "NavigateBackTool", + "NavigateTool", + "OpenAPISpec", + "ReadFileTool", + "SceneXplainTool", + "ShellTool", + "StructuredTool", + "Tool", + "VectorStoreQATool", + "VectorStoreQAWithSourcesTool", + "WikipediaQueryRun", + "WolframAlphaQueryRun", + "WriteFileTool", + "ZapierNLAListActions", + "ZapierNLARunAction", + "tool", +] + + +def test_public_api() -> None: + """Test for regressions or changes in the public API.""" + # Check that the public API is as expected + assert public_api == sorted(_EXPECTED)