From cf98f219f98a2561625fb21ebf809ef05d528642 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Sun, 18 Dec 2022 21:51:23 -0500 Subject: [PATCH] Harrison/tools exp (#372) --- docs/examples/agents.rst | 2 +- docs/examples/agents/custom_agent.ipynb | 142 ++++++++-- docs/examples/agents/mrkl.ipynb | 81 +++--- docs/examples/agents/react.ipynb | 19 +- .../agents/self_ask_with_search.ipynb | 10 +- docs/examples/chains/pal.ipynb | 2 +- docs/explanation/agents.md | 1 + docs/explanation/tools.md | 94 +++++++ docs/getting_started/agents.ipynb | 259 ++++++++++++++++-- docs/index.rst | 1 + langchain/agents/__init__.py | 8 +- langchain/agents/agent.py | 237 +++++++++------- langchain/agents/input.py | 44 --- langchain/agents/load_tools.py | 169 ++++++++++++ langchain/agents/loading.py | 7 +- langchain/agents/mrkl/base.py | 11 +- langchain/agents/mrkl/prompt.py | 3 +- langchain/agents/react/base.py | 25 +- langchain/agents/react/textworld_prompt.py | 7 +- langchain/agents/react/wiki_prompt.py | 7 +- langchain/agents/self_ask_with_search/base.py | 18 +- .../agents/self_ask_with_search/prompt.py | 7 +- langchain/chains/api/news_docs.py | 32 +++ langchain/chains/api/open_meteo_docs.py | 33 +++ langchain/chains/api/tmdb_docs.py | 37 +++ langchain/chains/base.py | 2 +- langchain/logger.py | 10 +- langchain/python.py | 10 +- langchain/schema.py | 7 + langchain/utilities/bash.py | 6 +- tests/integration_tests/chains/test_react.py | 2 +- .../chains/test_self_ask_with_search.py | 2 +- tests/unit_tests/agents/test_react.py | 15 +- tests/unit_tests/test_input.py | 75 ----- tests/unit_tests/test_python.py | 6 +- 35 files changed, 1024 insertions(+), 367 deletions(-) create mode 100644 docs/explanation/tools.md delete mode 100644 langchain/agents/input.py create mode 100644 langchain/agents/load_tools.py create mode 100644 langchain/chains/api/news_docs.py create mode 100644 langchain/chains/api/open_meteo_docs.py create mode 100644 langchain/chains/api/tmdb_docs.py delete mode 100644 tests/unit_tests/test_input.py diff --git a/docs/examples/agents.rst b/docs/examples/agents.rst index 7a6df17c..b1ae0724 100644 --- a/docs/examples/agents.rst +++ b/docs/examples/agents.rst @@ -4,7 +4,7 @@ Agents The examples here are all end-to-end agents for specific applications. In all examples there is an Agent with a particular set of tools. -- Tools: A tool can be anything that takes in a string and returns a string. This means that you can use both the primitives AND the chains found in `this `_ documentation. +- Tools: A tool can be anything that takes in a string and returns a string. This means that you can use both the primitives AND the chains found in `this `_ documentation. LangChain also provides a list of easily loadable tools. For detailed information on those, please see `this documentation <../explanation/tools.md>`_ - Agents: An agent uses an LLMChain to determine which tools to use. For a list of all available agent types, see `here <../explanation/agents.md>`_. **MRKL** diff --git a/docs/examples/agents/custom_agent.ipynb b/docs/examples/agents/custom_agent.ipynb index cc6f135b..189a1e14 100644 --- a/docs/examples/agents/custom_agent.ipynb +++ b/docs/examples/agents/custom_agent.ipynb @@ -28,7 +28,7 @@ "\n", "The first way to create a custom agent is to use an existing Agent class, but use a custom LLMChain. This is the simplest way to create a custom Agent. It is highly reccomended that you work with the `ZeroShotAgent`, as at the moment that is by far the most generalizable one. \n", "\n", - "Most of the work in creating the custom LLMChain comes down to the prompt. Because we are using an existing agent class to parse the output, it is very important that the prompt say to produce text in that format. However, besides those instructions, you can customize the prompt as you wish.\n", + "Most of the work in creating the custom LLMChain comes down to the prompt. Because we are using an existing agent class to parse the output, it is very important that the prompt say to produce text in that format. Additionally, we currently require an `agent_scratchpad` input variable to put notes on previous actions and observations. This should almost always be the final part of the prompt. However, besides those instructions, you can customize the prompt as you wish.\n", "\n", "To ensure that the prompt contains the appropriate instructions, we will utilize a helper method on that class. The helper method for the `ZeroShotAgent` takes the following arguments:\n", "\n", @@ -47,7 +47,7 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain.agents import ZeroShotAgent, Tool\n", + "from langchain.agents import ZeroShotAgent, Tool, AgentExecutor\n", "from langchain import OpenAI, SerpAPIWrapper, LLMChain" ] }, @@ -78,13 +78,14 @@ "prefix = \"\"\"Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:\"\"\"\n", "suffix = \"\"\"Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Args\"\n", "\n", - "Question: {input}\"\"\"\n", + "Question: {input}\n", + "{agent_scratchpad}\"\"\"\n", "\n", "prompt = ZeroShotAgent.create_prompt(\n", " tools, \n", " prefix=prefix, \n", " suffix=suffix, \n", - " input_variables=[\"input\"]\n", + " input_variables=[\"input\", \"agent_scratchpad\"]\n", ")" ] }, @@ -123,7 +124,8 @@ "\n", "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Args\"\n", "\n", - "Question: {input}\n" + "Question: {input}\n", + "{agent_scratchpad}\n" ] } ], @@ -148,12 +150,22 @@ "metadata": {}, "outputs": [], "source": [ - "agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools, verbose=True)" + "agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools)" ] }, { "cell_type": "code", "execution_count": 7, + "id": "490604e9", + "metadata": {}, + "outputs": [], + "source": [ + "agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, "id": "653b1617", "metadata": {}, "outputs": [ @@ -163,30 +175,126 @@ "text": [ "\n", "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "How many people live in canada?\n", - "Thought:\u001b[32;1m\u001b[1;3m I should look this up\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mThought: I need to find out how many people live in Canada\n", "Action: Search\n", - "Action Input: How many people live in canada\u001b[0m\n", - "Observation: \u001b[36;1m\u001b[1;3mThe current population of Canada is 38,533,678 as of Friday, November 25, 2022, based on Worldometer elaboration of the latest United Nations data. · Canada 2020 ...\u001b[0m\n", + "Action Input: Population of Canada\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mThe current population of Canada is 38,553,548 as of Saturday, December 17, 2022, based on Worldometer elaboration of the latest United Nations data. Canada ...\u001b[0m\n", "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n", - "Final Answer: Arrr, there be 38,533,678 people in Canada\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n" + "Final Answer: Arrr, there be 38,553,548 scallywags livin' in Canada!\u001b[0m\n", + "\u001b[1m> Finished AgentExecutor chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Arrr, there be 38,553,548 scallywags livin' in Canada!\"" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent_executor.run(\"How many people live in canada?\")" + ] + }, + { + "cell_type": "markdown", + "id": "040eb343", + "metadata": {}, + "source": [ + "### Multiple inputs\n", + "Agents can also work with prompts that require multiple inputs." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "43dbfa2f", + "metadata": {}, + "outputs": [], + "source": [ + "prefix = \"\"\"Answer the following questions as best you can. You have access to the following tools:\"\"\"\n", + "suffix = \"\"\"When answering, you MUST speak in the following language: {language}.\n", + "\n", + "Question: {input}\n", + "{agent_scratchpad}\"\"\"\n", + "\n", + "prompt = ZeroShotAgent.create_prompt(\n", + " tools, \n", + " prefix=prefix, \n", + " suffix=suffix, \n", + " input_variables=[\"input\", \"language\", \"agent_scratchpad\"]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0f087313", + "metadata": {}, + "outputs": [], + "source": [ + "llm_chain = LLMChain(llm=OpenAI(temperature=0), prompt=prompt)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "92c75a10", + "metadata": {}, + "outputs": [], + "source": [ + "agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ac5b83bf", + "metadata": {}, + "outputs": [], + "source": [ + "agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c960e4ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mThought: I should look up the population of Canada.\n", + "Action: Search\n", + "Action Input: Population of Canada\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mThe current population of Canada is 38,553,548 as of Saturday, December 17, 2022, based on Worldometer elaboration of the latest United Nations data. Canada ...\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer.\n", + "Final Answer: La popolazione attuale del Canada è 38.553.548 al sabato 17 dicembre 2022, secondo l'elaborazione di Worldometer dei dati più recenti delle Nazioni Unite.\u001b[0m\n", + "\u001b[1m> Finished AgentExecutor chain.\u001b[0m\n" ] }, { "data": { "text/plain": [ - "'Arrr, there be 38,533,678 people in Canada'" + "\"La popolazione attuale del Canada è 38.553.548 al sabato 17 dicembre 2022, secondo l'elaborazione di Worldometer dei dati più recenti delle Nazioni Unite.\"" ] }, - "execution_count": 7, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "agent.run(\"How many people live in canada?\")" + "agent_executor.run(input=\"How many people live in canada?\", language=\"italian\")" ] }, { @@ -224,7 +332,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/examples/agents/mrkl.ipynb b/docs/examples/agents/mrkl.ipynb index 71bc463d..5b99402a 100644 --- a/docs/examples/agents/mrkl.ipynb +++ b/docs/examples/agents/mrkl.ipynb @@ -46,7 +46,7 @@ " Tool(\n", " name = \"Search\",\n", " func=search.run,\n", - " description=\"useful for when you need to answer questions about current events\"\n", + " description=\"useful for when you need to answer questions about current events. You should ask targeted questions\"\n", " ),\n", " Tool(\n", " name=\"Calculator\",\n", @@ -56,7 +56,7 @@ " Tool(\n", " name=\"FooBar DB\",\n", " func=db_chain.run,\n", - " description=\"useful for when you need to answer questions about FooBar. Input should be in the form of a question\"\n", + " description=\"useful for when you need to answer questions about FooBar. Input should be in the form of a question containing full context\"\n", " )\n", "]" ] @@ -81,40 +81,44 @@ "name": "stdout", "output_type": "stream", "text": [ - "What is the age of Olivia Wilde's boyfriend raised to the 0.23 power?\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to find the age of Olivia Wilde's boyfriend\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentWithTools chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m I need to find out who Olivia Wilde's boyfriend is and then calculate his age raised to the 0.23 power.\n", "Action: Search\n", - "Action Input: \"Olivia Wilde's boyfriend\"\u001b[0m\n", + "Action Input: \"Who is Olivia Wilde's boyfriend?\"\u001b[0m\n", "Observation: \u001b[36;1m\u001b[1;3mOlivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to find the age of Harry Styles\n", + "Thought:\u001b[32;1m\u001b[1;3m I need to find out Harry Styles' age.\n", "Action: Search\n", - "Action Input: \"Harry Styles age\"\u001b[0m\n", + "Action Input: \"How old is Harry Styles?\"\u001b[0m\n", "Observation: \u001b[36;1m\u001b[1;3m28 years\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to calculate 28 to the 0.23 power\n", + "Thought:\u001b[32;1m\u001b[1;3m I need to calculate 28 raised to the 0.23 power.\n", "Action: Calculator\n", "Action Input: 28^0.23\u001b[0m\n", "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", "28^0.23\u001b[32;1m\u001b[1;3m\n", "\n", "```python\n", - "print(28**0.23)\n", + "import math\n", + "print(math.pow(28, 0.23))\n", "```\n", "\u001b[0m\n", "Answer: \u001b[33;1m\u001b[1;3m2.1520202182226886\n", "\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n", + "\u001b[1m> Finished LLMMathChain chain.\u001b[0m\n", "\n", "Observation: \u001b[33;1m\u001b[1;3mAnswer: 2.1520202182226886\n", "\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n", - "Final Answer: 2.1520202182226886\u001b[0m" + "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer.\n", + "Final Answer: Harry Styles, Olivia Wilde's boyfriend, is 28 years old and his age raised to the 0.23 power is 2.1520202182226886.\u001b[0m\n", + "\u001b[1m> Finished AgentWithTools chain.\u001b[0m\n" ] }, { "data": { "text/plain": [ - "'2.1520202182226886'" + "\"Harry Styles, Olivia Wilde's boyfriend, is 28 years old and his age raised to the 0.23 power is 2.1520202182226886.\"" ] }, "execution_count": 4, @@ -123,7 +127,7 @@ } ], "source": [ - "mrkl.run(\"What is the age of Olivia Wilde's boyfriend raised to the 0.23 power?\")" + "mrkl.run(\"Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?\")" ] }, { @@ -136,43 +140,34 @@ "name": "stdout", "output_type": "stream", "text": [ - "Who recently released an album called 'The Storm Before the Calm' and are they in the FooBar database? If so, what albums of theirs are in the FooBar database?\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to find an album called 'The Storm Before the Calm'\n", - "Action: Search\n", - "Action Input: \"The Storm Before the Calm album\"\u001b[0m\n", - "Observation: \u001b[36;1m\u001b[1;3mThe Storm Before the Calm (stylized in all lowercase) is the tenth (and eighth international) studio album by Canadian-American singer-songwriter Alanis ...\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to check if Alanis is in the FooBar database\n", - "Action: FooBar DB\n", - "Action Input: \"Does Alanis Morissette exist in the FooBar database?\"\u001b[0m\n", "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "Does Alanis Morissette exist in the FooBar database?\n", - "SQLQuery:\u001b[32;1m\u001b[1;3m SELECT * FROM Artist WHERE Name = 'Alanis Morissette'\u001b[0m\n", - "SQLResult: \u001b[33;1m\u001b[1;3m[(4, 'Alanis Morissette')]\u001b[0m\n", - "Answer:\u001b[32;1m\u001b[1;3m Yes\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n", "\n", - "Observation: \u001b[38;5;200m\u001b[1;3m Yes\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to find out what albums of Alanis's are in the FooBar database\n", + "\u001b[1m> Entering new AgentWithTools chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m I need to find out the artist's full name and then search the FooBar database for their albums.\n", + "Action: Search\n", + "Action Input: \"The Storm Before the Calm\" artist\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mThe Storm Before the Calm (stylized in all lowercase) is the tenth (and eighth international) studio album by Canadian-American singer-songwriter Alanis ...\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m I now need to search the FooBar database for Alanis Morissette's albums\n", "Action: FooBar DB\n", - "Action Input: \"What albums by Alanis Morissette are in the FooBar database?\"\u001b[0m\n", + "Action Input: What albums by Alanis Morissette are in the FooBar database?\u001b[0m\n", "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", - "What albums by Alanis Morissette are in the FooBar database?\n", - "SQLQuery:\u001b[32;1m\u001b[1;3m SELECT Album.Title FROM Album JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Artist.Name = 'Alanis Morissette'\u001b[0m\n", + "\u001b[1m> Entering new SQLDatabaseChain chain...\u001b[0m\n", + "What albums by Alanis Morissette are in the FooBar database? \n", + "SQLQuery:\u001b[32;1m\u001b[1;3m SELECT Title FROM Album WHERE ArtistId IN (SELECT ArtistId FROM Artist WHERE Name = 'Alanis Morissette');\u001b[0m\n", "SQLResult: \u001b[33;1m\u001b[1;3m[('Jagged Little Pill',)]\u001b[0m\n", - "Answer:\u001b[32;1m\u001b[1;3m Jagged Little Pill\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n", + "Answer:\u001b[32;1m\u001b[1;3m The album Jagged Little Pill by Alanis Morissette is in the FooBar database.\u001b[0m\n", + "\u001b[1m> Finished SQLDatabaseChain chain.\u001b[0m\n", "\n", - "Observation: \u001b[38;5;200m\u001b[1;3m Jagged Little Pill\u001b[0m\n", + "Observation: \u001b[38;5;200m\u001b[1;3m The album Jagged Little Pill by Alanis Morissette is in the FooBar database.\u001b[0m\n", "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n", - "Final Answer: The album is by Alanis Morissette and the albums in the FooBar database by her are Jagged Little Pill\u001b[0m" + "Final Answer: Alanis Morissette and Jagged Little Pill are in the FooBar database.\u001b[0m\n", + "\u001b[1m> Finished AgentWithTools chain.\u001b[0m\n" ] }, { "data": { "text/plain": [ - "'The album is by Alanis Morissette and the albums in the FooBar database by her are Jagged Little Pill'" + "'Alanis Morissette and Jagged Little Pill are in the FooBar database.'" ] }, "execution_count": 5, @@ -181,13 +176,13 @@ } ], "source": [ - "mrkl.run(\"Who recently released an album called 'The Storm Before the Calm' and are they in the FooBar database? If so, what albums of theirs are in the FooBar database?\")" + "mrkl.run(\"What is the full name of the artist who recently released an album called 'The Storm Before the Calm' and are they in the FooBar database? If so, what albums of theirs are in the FooBar database?\")" ] }, { "cell_type": "code", "execution_count": null, - "id": "d7c2e6ac", + "id": "af016a70", "metadata": {}, "outputs": [], "source": [] @@ -209,7 +204,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/examples/agents/react.ipynb b/docs/examples/agents/react.ipynb index 4af4c672..be4f1834 100644 --- a/docs/examples/agents/react.ipynb +++ b/docs/examples/agents/react.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "id": "4e272b47", "metadata": {}, "outputs": [], @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 2, "id": "8078c8f1", "metadata": {}, "outputs": [ @@ -48,18 +48,17 @@ "text": [ "\n", "\n", - "\u001b[1m> Entering new ReActDocstoreAgent chain...\u001b[0m\n", - "Author David Chanoff has collaborated with a U.S. Navy admiral who served as the ambassador to the United Kingdom under which President?\n", - "Thought 1:\u001b[32;1m\u001b[1;3m I need to search David Chanoff and find the U.S. Navy admiral he collaborated\n", - "with.\n", + "\u001b[1m> Entering new AgentWithTools chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m\n", + "Thought 1: I need to search David Chanoff and find the U.S. Navy admiral he collaborated with.\n", "Action 1: Search[David Chanoff]\u001b[0m\n", "Observation 1: \u001b[36;1m\u001b[1;3mDavid Chanoff is a noted author of non-fiction work. His work has typically involved collaborations with the principal protagonist of the work concerned. His collaborators have included; Augustus A. White, Joycelyn Elders, Đoàn Văn Toại, William J. Crowe, Ariel Sharon, Kenneth Good and Felix Zandman. He has also written about a wide range of subjects including literary history, education and foreign for The Washington Post, The New Republic and The New York Times Magazine. He has published more than twelve books.\u001b[0m\n", "Thought 2:\u001b[32;1m\u001b[1;3m The U.S. Navy admiral David Chanoff collaborated with is William J. Crowe.\n", "Action 2: Search[William J. Crowe]\u001b[0m\n", "Observation 2: \u001b[36;1m\u001b[1;3mWilliam James Crowe Jr. (January 2, 1925 – October 18, 2007) was a United States Navy admiral and diplomat who served as the 11th chairman of the Joint Chiefs of Staff under Presidents Ronald Reagan and George H. W. Bush, and as the ambassador to the United Kingdom and Chair of the Intelligence Oversight Board under President Bill Clinton.\u001b[0m\n", - "Thought 3:\u001b[32;1m\u001b[1;3m William J. Crowe served as the ambassador to the United Kingdom under President Bill Clinton.\n", + "Thought 3:\u001b[32;1m\u001b[1;3m The President William J. Crowe served as the ambassador to the United Kingdom under is Bill Clinton.\n", "Action 3: Finish[Bill Clinton]\u001b[0m\n", - "\u001b[1m> Finished ReActDocstoreAgent chain.\u001b[0m\n" + "\u001b[1m> Finished AgentWithTools chain.\u001b[0m\n" ] }, { @@ -68,7 +67,7 @@ "'Bill Clinton'" ] }, - "execution_count": 7, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -81,7 +80,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4ff64e81", + "id": "75f914ba", "metadata": {}, "outputs": [], "source": [] diff --git a/docs/examples/agents/self_ask_with_search.ipynb b/docs/examples/agents/self_ask_with_search.ipynb index d4a56ab2..305e28f9 100644 --- a/docs/examples/agents/self_ask_with_search.ipynb +++ b/docs/examples/agents/self_ask_with_search.ipynb @@ -22,15 +22,14 @@ "text": [ "\n", "\n", - "\u001b[1m> Entering new SelfAskWithSearchAgent chain...\u001b[0m\n", - "What is the hometown of the reigning men's U.S. Open champion?\n", - "Are follow up questions needed here:\u001b[32;1m\u001b[1;3m Yes.\n", + "\u001b[1m> Entering new AgentWithTools chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m Yes.\n", "Follow up: Who is the reigning men's U.S. Open champion?\u001b[0m\n", "Intermediate answer: \u001b[36;1m\u001b[1;3mCarlos Alcaraz\u001b[0m\n", "\u001b[32;1m\u001b[1;3mFollow up: Where is Carlos Alcaraz from?\u001b[0m\n", "Intermediate answer: \u001b[36;1m\u001b[1;3mEl Palmar, Spain\u001b[0m\n", "\u001b[32;1m\u001b[1;3mSo the final answer is: El Palmar, Spain\u001b[0m\n", - "\u001b[1m> Finished SelfAskWithSearchAgent chain.\u001b[0m\n" + "\u001b[1m> Finished AgentWithTools chain.\u001b[0m\n" ] }, { @@ -58,7 +57,6 @@ "]\n", "\n", "self_ask_with_search = initialize_agent(tools, llm, agent=\"self-ask-with-search\", verbose=True)\n", - "\n", "self_ask_with_search.run(\"What is the hometown of the reigning men's U.S. Open champion?\")" ] }, @@ -87,7 +85,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/examples/chains/pal.ipynb b/docs/examples/chains/pal.ipynb index f15b8afb..7be54735 100644 --- a/docs/examples/chains/pal.ipynb +++ b/docs/examples/chains/pal.ipynb @@ -172,7 +172,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.7" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/explanation/agents.md b/docs/explanation/agents.md index 80dcd7f8..0f632ee7 100644 --- a/docs/explanation/agents.md +++ b/docs/explanation/agents.md @@ -2,6 +2,7 @@ Agents use an LLM to determine which actions to take and in what order. An action can either be using a tool and observing its output, or returning to the user. +For a list of easily loadable tools, see [here](tools.md). Here are the agents available in LangChain. For a tutorial on how to load agents, see [here](/getting_started/agents.ipynb). diff --git a/docs/explanation/tools.md b/docs/explanation/tools.md new file mode 100644 index 00000000..cb9da47b --- /dev/null +++ b/docs/explanation/tools.md @@ -0,0 +1,94 @@ +# Tools + +Tools are functions that agents can use to interact with the world. +These tools can be generic utilities (eg search), other chains, or even other agents. + +Currently, tools can be loaded with the following snippet: + +```python +from langchain.agents import load_tools +tool_names = [...] +tools = load_tools(tool_names) +``` + +Some tools (eg chains, agents) may require a base LLM to use to initialize them. +In that case, you can pass in an LLM as well: + +```python +from langchain.agents import load_tools +tool_names = [...] +llm = ... +tools = load_tools(tool_names, llm=llm) +``` + +Below is a list of all supported tools and relevant information: +- Tool Name: The name the LLM refers to the tool by. +- Tool Description: The description of the tool that is passed to the LLM. +- Notes: Notes about the tool that are NOT passed to the LLM. +- Requires LLM: Whether this tool requires an LLM to be initialized. +- (Optional) Extra Parameters: What extra parameters are required to initialize this tool. + +### List of Tools + +**python_repl** +- Tool Name: Python REPL +- Tool Description: A Python shell. Use this to execute python commands. Input should be a valid python command. If you expect output it should be printed out. +- Notes: Maintains state. +- Requires LLM: No + + +**serpapi** +- Tool Name: Search +- Tool Description: A search engine. Useful for when you need to answer questions about current events. Input should be a search query. +- Notes: Calls the Serp API and then parses results. +- Requires LLM: No + +**requests** +- Tool Name: Requests +- Tool Description: A portal to the internet. Use this when you need to get specific content from a site. Input should be a specific url, and the output will be all the text on that page. +- Notes: Uses the Python requests module. +- Requires LLM: No + +**terminal** +- Tool Name: Terminal +- Tool Description: Executes commands in a terminal. Input should be valid commands, and the output will be any output from running that command. +- Notes: Executes commands with subprocess. +- Requires LLM: No + +**pal-math** +- Tool Name: PAL-MATH +- Tool Description: A language model that is really good at solving complex word math problems. Input should be a fully worded hard word math problem. +- Notes: Based on [this paper](https://arxiv.org/pdf/2211.10435.pdf). +- Requires LLM: Yes + +**pal-colored-objects** +- Tool Name: PAL-COLOR-OBJ +- Tool Description: A language model that is really good at reasoning about position and the color attributes of objects. Input should be a fully worded hard reasoning problem. Make sure to include all information about the objects AND the final question you want to answer. +- Notes: Based on [this paper](https://arxiv.org/pdf/2211.10435.pdf). +- Requires LLM: Yes + +**llm-math** +- Tool Name: Calculator +- Tool Description: Useful for when you need to answer questions about math. +- Notes: An instance of the `LLMMath` chain. +- Requires LLM: Yes + +**open-meteo-api** +- Tool Name: Open Meteo API +- Tool Description: Useful for when you want to get weather information from the OpenMeteo API. The input should be a question in natural language that this API can answer. +- Notes: A natural language connection to the Open Meteo API (`https://api.open-meteo.com/`), specifically the `/v1/forecast` endpoint. +- Requires LLM: Yes + +**news-api** +- Tool Name: News API +- Tool Description: Use this when you want to get information about the top headlines of current news stories. The input should be a question in natural language that this API can answer. +- Notes: A natural language connection to the News API (`https://newsapi.org`), specifically the `/v2/top-headlines` endpoint. +- Requires LLM: Yes +- Extra Parameters: `news_api_key` (your API key to access this endpoint) + +**tmdb-api** +- Tool Name: TMDB API +- Tool Description: Useful for when you want to get information from The Movie Database. The input should be a question in natural language that this API can answer. +- Notes: A natural language connection to the TMDB API (`https://api.themoviedb.org/3`), specifically the `/search/movie` endpoint. +- Requires LLM: Yes +- Extra Parameters: `tmdb_bearer_token` (your Bearer Token to access this endpoint - note that this is different than the API key) diff --git a/docs/getting_started/agents.ipynb b/docs/getting_started/agents.ipynb index d60030d0..6e8c85e7 100644 --- a/docs/getting_started/agents.ipynb +++ b/docs/getting_started/agents.ipynb @@ -10,7 +10,7 @@ "Agents use an LLM to determine which actions to take and in what order.\n", "An action can either be using a tool and observing its output, or returning to the user.\n", "\n", - "When used correctly agents can be extremely powerful. The purpose of this notebook is to show you how to easily use agents through the simplest, highest level API. If you want more low level control over various components, check out the documentation for custom agents (coming soon)." + "When used correctly agents can be extremely powerful. The purpose of this notebook is to show you how to easily use agents through the simplest, highest level API. If you want more low level control over various components, check out the documentation for [custom agents](../examples/agents/custom_agents.ipynb)." ] }, { @@ -26,7 +26,9 @@ "- LLM: The language model powering the agent.\n", "- Agent: The agent to use. This should be a string that references a support agent class. Because this notebook focuses on the simplest, highest level API, this only covers using the standard supported agents. If you want to implement a custom agent, see the documentation for custom agents (coming soon).\n", "\n", - "**For a list of supported agents and their specifications, see [here](../explanation/agents.md)**" + "**Agents**: For a list of supported agents and their specifications, see [here](../explanation/agents.md).\n", + "\n", + "**Tools**: For a list of predefined tools and their specifications, see [here](../explanation/tools.md)." ] }, { @@ -46,7 +48,9 @@ " description: Optional[str] = None\n", "```\n", "\n", - "The two required components of a Tool are the name and then the tool itself. A tool description is optional, as it is needed for some agents but not all." + "The two required components of a Tool are the name and then the tool itself. A tool description is optional, as it is needed for some agents but not all.\n", + "\n", + "Besides defining tools yourself, you can also take advantage of predefined tools. For instructions on how to do that, please see later on in this notebook." ] }, { @@ -110,7 +114,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 11, "id": "6f96a891", "metadata": {}, "outputs": [ @@ -118,57 +122,272 @@ "name": "stdout", "output_type": "stream", "text": [ - "What is the age of Olivia Wilde's boyfriend raised to the 0.23 power?\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to find the age of Olivia Wilde's boyfriend\n", + "\n", + "\n", + "\u001b[1m> Entering new AgentWithTools chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m I need to find out who Olivia Wilde's boyfriend is and then calculate his age raised to the 0.23 power.\n", "Action: Search\n", - "Action Input: \"Olivia Wilde's boyfriend\"\u001b[0m\n", + "Action Input: Olivia Wilde's boyfriend\u001b[0m\n", "Observation: \u001b[36;1m\u001b[1;3mOlivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to find the age of Harry Styles\n", + "Thought:\u001b[32;1m\u001b[1;3m I need to find out Harry Styles' age.\n", "Action: Search\n", - "Action Input: \"Harry Styles age\"\u001b[0m\n", + "Action Input: Harry Styles age\u001b[0m\n", "Observation: \u001b[36;1m\u001b[1;3m28 years\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I need to calculate 28 to the 0.23 power\n", + "Thought:\u001b[32;1m\u001b[1;3m I need to calculate 28 raised to the 0.23 power.\n", "Action: Calculator\n", "Action Input: 28^0.23\u001b[0m\n", "\n", - "\u001b[1m> Entering new chain...\u001b[0m\n", + "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", "28^0.23\u001b[32;1m\u001b[1;3m\n", "\n", "```python\n", - "print(28**0.23)\n", + "import math\n", + "print(math.pow(28, 0.23))\n", "```\n", "\u001b[0m\n", "Answer: \u001b[33;1m\u001b[1;3m2.1520202182226886\n", "\u001b[0m\n", - "\u001b[1m> Finished chain.\u001b[0m\n", + "\u001b[1m> Finished LLMMathChain chain.\u001b[0m\n", "\n", "Observation: \u001b[33;1m\u001b[1;3mAnswer: 2.1520202182226886\n", "\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer\n", - "Final Answer: 2.1520202182226886\u001b[0m" + "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer.\n", + "Final Answer: Harry Styles, Olivia Wilde's boyfriend, is 28 years old and his age raised to the 0.23 power is 2.1520202182226886.\u001b[0m\n", + "\u001b[1m> Finished AgentWithTools chain.\u001b[0m\n" ] }, { "data": { "text/plain": [ - "'2.1520202182226886'" + "\"Harry Styles, Olivia Wilde's boyfriend, is 28 years old and his age raised to the 0.23 power is 2.1520202182226886.\"" ] }, - "execution_count": 4, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "agent.run(\"How old is Olivia Wilde's boyfriend? What is that number raised to the 0.23 power?\")" + "agent.run(\"Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?\")" + ] + }, + { + "cell_type": "markdown", + "id": "0a70f2fc", + "metadata": {}, + "source": [ + "## Intermediate Steps\n", + "In order to get more visibility into what an agent is doing, we can also return intermediate steps. This comes in the form of an extra key in the return value, which is a list of (action, observation) tuples." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "2f0852ff", "metadata": {}, "outputs": [], + "source": [ + "llm = OpenAI(temperature=0)\n", + "agent = initialize_agent(tools, llm, agent=\"zero-shot-react-description\", verbose=True, return_intermediate_steps=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "837211e8", + "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 I need to find out how old Olivia Wilde's boyfriend is, and then use a calculator to calculate the power.\n", + "Action: Search\n", + "Action Input: Olivia Wilde's boyfriend age\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mWhile Wilde, 37, and Styles, 27, have both kept a low profile when it comes to talking about their relationship, Wilde did address their ...\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m Olivia Wilde's boyfriend is 27 years old.\n", + "Action: Calculator\n", + "Action Input: 27^0.23\u001b[0m\n", + "\n", + "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", + "27^0.23\u001b[32;1m\u001b[1;3m\n", + "\n", + "```python\n", + "import math\n", + "print(math.pow(27, 0.23))\n", + "```\n", + "\u001b[0m\n", + "Answer: \u001b[33;1m\u001b[1;3m2.1340945944237553\n", + "\u001b[0m\n", + "\u001b[1m> Finished LLMMathChain chain.\u001b[0m\n", + "\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 2.1340945944237553\n", + "\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer.\n", + "Final Answer: 2.1340945944237553\u001b[0m\n", + "\u001b[1m> Finished AgentExecutor chain.\u001b[0m\n" + ] + } + ], + "source": [ + "response = agent({\"input\":\"How old is Olivia Wilde's boyfriend? What is that number raised to the 0.23 power?\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "e1a39a23", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(AgentAction(tool='Search', tool_input=\"Olivia Wilde's boyfriend age\", log=\" I need to find out how old Olivia Wilde's boyfriend is, and then use a calculator to calculate the power.\\nAction: Search\\nAction Input: Olivia Wilde's boyfriend age\"), 'While Wilde, 37, and Styles, 27, have both kept a low profile when it comes to talking about their relationship, Wilde did address their ...'), (AgentAction(tool='Calculator', tool_input='27^0.23', log=\" Olivia Wilde's boyfriend is 27 years old.\\nAction: Calculator\\nAction Input: 27^0.23\"), 'Answer: 2.1340945944237553\\n')]\n" + ] + } + ], + "source": [ + "# The actual return type is a NamedTuple for the agent action, and then an observation\n", + "print(response[\"intermediate_steps\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6365bb69", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " [\n", + " [\n", + " \"Search\",\n", + " \"Olivia Wilde's boyfriend age\",\n", + " \" I need to find out how old Olivia Wilde's boyfriend is, and then use a calculator to calculate the power.\\nAction: Search\\nAction Input: Olivia Wilde's boyfriend age\"\n", + " ],\n", + " \"While Wilde, 37, and Styles, 27, have both kept a low profile when it comes to talking about their relationship, Wilde did address their ...\"\n", + " ],\n", + " [\n", + " [\n", + " \"Calculator\",\n", + " \"27^0.23\",\n", + " \" Olivia Wilde's boyfriend is 27 years old.\\nAction: Calculator\\nAction Input: 27^0.23\"\n", + " ],\n", + " \"Answer: 2.1340945944237553\\n\"\n", + " ]\n", + "]\n" + ] + } + ], + "source": [ + "import json\n", + "print(json.dumps(response[\"intermediate_steps\"], indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "d20f9358", + "metadata": {}, + "source": [ + "## Predefined Tools\n", + "In addition to letting you define arbitrary tools, LangChain also provides some predefined tools. These can be either generic utility functions, chains, or other agents. You can easily load these tools by name (optionally specifying an LLM to use, if the tool requires it). For a list of all possible tools, please see [here](../explanation/tools.md).\n", + "\n", + "Let's recreate the above example loading tools by name. Because the LLMMath chain needs an LLM, we need to specify one." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ba4e7618", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.agents import load_tools\n", + "tools = load_tools([\"serpapi\", \"llm-math\"], llm=llm)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "03208e2b", + "metadata": {}, + "outputs": [], + "source": [ + "agent = initialize_agent(tools, llm, agent=\"zero-shot-react-description\", verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "244ee75c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new AgentWithTools chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3m I need to find out who Olivia Wilde's boyfriend is and then calculate his age raised to the 0.23 power.\n", + "Action: Search\n", + "Action Input: Olivia Wilde's boyfriend\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mOlivia Wilde started dating Harry Styles after ending her years-long engagement to Jason Sudeikis — see their relationship timeline.\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m I need to find out Harry Styles' age.\n", + "Action: Search\n", + "Action Input: Harry Styles age\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3m28 years\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m I need to calculate 28 raised to the 0.23 power.\n", + "Action: Calculator\n", + "Action Input: 28^0.23\u001b[0m\n", + "\n", + "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", + "28^0.23\u001b[32;1m\u001b[1;3m\n", + "\n", + "```python\n", + "import math\n", + "print(math.pow(28, 0.23))\n", + "```\n", + "\u001b[0m\n", + "Answer: \u001b[33;1m\u001b[1;3m2.1520202182226886\n", + "\u001b[0m\n", + "\u001b[1m> Finished LLMMathChain chain.\u001b[0m\n", + "\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 2.1520202182226886\n", + "\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3m I now know the final answer.\n", + "Final Answer: Harry Styles, Olivia Wilde's boyfriend, is 28 years old and his age raised to the 0.23 power is 2.1520202182226886.\u001b[0m\n", + "\u001b[1m> Finished AgentWithTools chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Harry Styles, Olivia Wilde's boyfriend, is 28 years old and his age raised to the 0.23 power is 2.1520202182226886.\"" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.run(\"Who is Olivia Wilde's boyfriend? What is his current age raised to the 0.23 power?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7776981", + "metadata": {}, + "outputs": [], "source": [] } ], @@ -188,7 +407,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.10.8" } }, "nbformat": 4, diff --git a/docs/index.rst b/docs/index.rst index 51dae342..34a48b14 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -161,6 +161,7 @@ see detailed information about the various classes, methods, and APIs. explanation/core_concepts.md explanation/combine_docs.md explanation/agents.md + explanation/tools.md explanation/glossary.md explanation/cool_demos.md Discord diff --git a/langchain/agents/__init__.py b/langchain/agents/__init__.py index 6c41e16b..ada59a03 100644 --- a/langchain/agents/__init__.py +++ b/langchain/agents/__init__.py @@ -1,5 +1,6 @@ -"""Routing chains.""" -from langchain.agents.agent import Agent +"""Interface for agents.""" +from langchain.agents.agent import Agent, AgentExecutor +from langchain.agents.load_tools import get_all_tool_names, load_tools from langchain.agents.loading import initialize_agent from langchain.agents.mrkl.base import MRKLChain, ZeroShotAgent from langchain.agents.react.base import ReActChain, ReActTextWorldAgent @@ -10,9 +11,12 @@ __all__ = [ "MRKLChain", "SelfAskWithSearchChain", "ReActChain", + "AgentExecutor", "Agent", "Tool", "initialize_agent", "ZeroShotAgent", "ReActTextWorldAgent", + "load_tools", + "get_all_tool_names", ] diff --git a/langchain/agents/agent.py b/langchain/agents/agent.py index d08bbe18..5dc095ed 100644 --- a/langchain/agents/agent.py +++ b/langchain/agents/agent.py @@ -1,45 +1,115 @@ """Chain that takes in an input and produces an action and action input.""" from __future__ import annotations -from abc import ABC, abstractmethod -from typing import Any, ClassVar, Dict, List, Optional, Tuple +import logging +from abc import abstractmethod +from typing import Any, Dict, List, Optional, Tuple, Union -from pydantic import BaseModel +from pydantic import BaseModel, root_validator -from langchain.agents.input import ChainedInput +import langchain from langchain.agents.tools import Tool from langchain.chains.base import Chain from langchain.chains.llm import LLMChain from langchain.input import get_color_mapping from langchain.llms.base import BaseLLM from langchain.prompts.base import BasePromptTemplate -from langchain.schema import AgentAction +from langchain.prompts.few_shot import FewShotPromptTemplate +from langchain.prompts.prompt import PromptTemplate +from langchain.schema import AgentAction, AgentFinish +logger = logging.getLogger() -class Agent(Chain, BaseModel, ABC): - """Agent that uses an LLM.""" - prompt: ClassVar[BasePromptTemplate] +class Agent(BaseModel): + """Class responsible for calling the language model and deciding the action. + + This is driven by an LLMChain. The prompt in the LLMChain MUST include + a variable called "agent_scratchpad" where the agent can put its + intermediary work. + """ + llm_chain: LLMChain - tools: List[Tool] - input_key: str = "input" #: :meta private: - output_key: str = "output" #: :meta private: + return_values: List[str] = ["output"] + + @abstractmethod + def _extract_tool_and_input(self, text: str) -> Optional[Tuple[str, str]]: + """Extract tool and tool input from llm output.""" + + def _fix_text(self, text: str) -> str: + """Fix the text.""" + raise ValueError("fix_text not implemented for this agent.") @property - def input_keys(self) -> List[str]: - """Return the singular input key. + def _stop(self) -> List[str]: + return [f"\n{self.observation_prefix}"] - :meta private: + def plan( + self, intermediate_steps: List[Tuple[AgentAction, str]], **kwargs: Any + ) -> Union[AgentFinish, AgentAction]: + """Given input, decided what to do. + + Args: + intermediate_steps: Steps the LLM has taken to date, + along with observations + **kwargs: User inputs. + + Returns: + Action specifying what tool to use. """ - return [self.input_key] + thoughts = "" + for action, observation in intermediate_steps: + thoughts += action.log + thoughts += f"\n{self.observation_prefix}{observation}\n{self.llm_prefix}" + new_inputs = {"agent_scratchpad": thoughts, "stop": self._stop} + full_inputs = {**kwargs, **new_inputs} + full_output = self.llm_chain.predict(**full_inputs) + parsed_output = self._extract_tool_and_input(full_output) + while parsed_output is None: + full_output = self._fix_text(full_output) + full_inputs["agent_scratchpad"] += full_output + output = self.llm_chain.predict(**full_inputs) + full_output += output + parsed_output = self._extract_tool_and_input(full_output) + tool, tool_input = parsed_output + if tool == self.finish_tool_name: + return AgentFinish({"output": tool_input}, full_output) + return AgentAction(tool, tool_input, full_output) + + def prepare_for_new_call(self) -> None: + """Prepare the agent for new call, if needed.""" + pass @property - def output_keys(self) -> List[str]: - """Return the singular output key. + def finish_tool_name(self) -> str: + """Name of the tool to use to finish the chain.""" + return "Final Answer" + + @property + def input_keys(self) -> List[str]: + """Return the input keys. :meta private: """ - return [self.output_key] + return list(set(self.llm_chain.input_keys) - {"agent_scratchpad"}) + + @root_validator() + def validate_prompt(cls, values: Dict) -> Dict: + """Validate that prompt matches format.""" + prompt = values["llm_chain"].prompt + if "agent_scratchpad" not in prompt.input_variables: + logger.warning( + "`agent_scratchpad` should be a variable in prompt.input_variables." + " Did not find it, so adding it at the end." + ) + prompt.input_variables.append("agent_scratchpad") + if isinstance(prompt, PromptTemplate): + prompt.template += "\n{agent_scratchpad}" + elif isinstance(prompt, FewShotPromptTemplate): + prompt.suffix += "\n{agent_scratchpad}" + else: + raise ValueError(f"Got unexpected prompt type {type(prompt)}") + return values @property @abstractmethod @@ -51,27 +121,10 @@ class Agent(Chain, BaseModel, ABC): def llm_prefix(self) -> str: """Prefix to append the LLM call with.""" - @property - def finish_tool_name(self) -> str: - """Name of the tool to use to finish the chain.""" - return "Final Answer" - - @property - def starter_string(self) -> str: - """Put this string after user input but before first LLM call.""" - return "\n" - + @classmethod @abstractmethod - def _extract_tool_and_input(self, text: str) -> Optional[Tuple[str, str]]: - """Extract tool and tool input from llm output.""" - - def _fix_text(self, text: str) -> str: - """Fix the text.""" - raise ValueError("fix_text not implemented for this agent.") - - @property - def _stop(self) -> List[str]: - return [f"\n{self.observation_prefix}"] + def create_prompt(cls, tools: List[Tool]) -> BasePromptTemplate: + """Create a prompt for this class.""" @classmethod def _validate_tools(cls, tools: List[Tool]) -> None: @@ -79,73 +132,72 @@ class Agent(Chain, BaseModel, ABC): pass @classmethod - def create_prompt(cls, tools: List[Tool]) -> BasePromptTemplate: - """Create a prompt for this class.""" - return cls.prompt - - def _prepare_for_new_call(self) -> None: - pass - - @classmethod - def from_llm_and_tools( - cls, llm: BaseLLM, tools: List[Tool], **kwargs: Any - ) -> Agent: + def from_llm_and_tools(cls, llm: BaseLLM, tools: List[Tool]) -> Agent: """Construct an agent from an LLM and tools.""" cls._validate_tools(tools) llm_chain = LLMChain(llm=llm, prompt=cls.create_prompt(tools)) - return cls(llm_chain=llm_chain, tools=tools, **kwargs) + return cls(llm_chain=llm_chain) - def get_action(self, text: str) -> AgentAction: - """Given input, decided what to do. - Args: - text: input string +class AgentExecutor(Chain, BaseModel): + """Consists of an agent using tools.""" - Returns: - Action specifying what tool to use. + agent: Agent + tools: List[Tool] + return_intermediate_steps: bool = False + + @classmethod + def from_agent_and_tools( + cls, agent: Agent, tools: List[Tool], **kwargs: Any + ) -> AgentExecutor: + """Create from agent and tools.""" + return cls(agent=agent, tools=tools, **kwargs) + + @property + def input_keys(self) -> List[str]: + """Return the input keys. + + :meta private: """ - input_key = self.llm_chain.input_keys[0] - inputs = {input_key: text, "stop": self._stop} - full_output = self.llm_chain.predict(**inputs) - parsed_output = self._extract_tool_and_input(full_output) - while parsed_output is None: - full_output = self._fix_text(full_output) - inputs = {input_key: text + full_output, "stop": self._stop} - output = self.llm_chain.predict(**inputs) - full_output += output - parsed_output = self._extract_tool_and_input(full_output) - tool, tool_input = parsed_output - return AgentAction(tool, tool_input, full_output) + return self.agent.input_keys - def _call(self, inputs: Dict[str, str]) -> Dict[str, str]: + @property + def output_keys(self) -> List[str]: + """Return the singular output key. + + :meta private: + """ + if self.return_intermediate_steps: + return self.agent.return_values + ["intermediate_steps"] + else: + return self.agent.return_values + + def _call(self, inputs: Dict[str, str]) -> Dict[str, Any]: """Run text through and get agent response.""" - text = inputs[self.input_key] # Do any preparation necessary when receiving a new input. - self._prepare_for_new_call() + self.agent.prepare_for_new_call() # Construct a mapping of tool name to tool for easy lookup name_to_tool_map = {tool.name: tool.func for tool in self.tools} - # Construct the initial string to pass into the LLM. This is made up - # of the user input, the special starter string, and then the LLM prefix. - # The starter string is a special string that may be used by a LLM to - # immediately follow the user input. The LLM prefix is a string that - # prompts the LLM to take an action. - starter_string = text + self.starter_string + self.llm_prefix - # We use the ChainedInput class to iteratively add to the input over time. - chained_input = ChainedInput(starter_string, verbose=self.verbose) # We construct a mapping from each tool to a color, used for logging. color_mapping = get_color_mapping( [tool.name for tool in self.tools], excluded_colors=["green"] ) + intermediate_steps: List[Tuple[AgentAction, str]] = [] # We now enter the agent loop (until it returns something). while True: # Call the LLM to see what to do. - output = self.get_action(chained_input.input) - # Add the log to the Chained Input. - chained_input.add_action(output, color="green") + output = self.agent.plan(intermediate_steps, **inputs) # If the tool chosen is the finishing tool, then we end and return. - if output.tool == self.finish_tool_name: - return {self.output_key: output.tool_input} - # Otherwise we lookup the tool + if isinstance(output, AgentFinish): + if self.verbose: + langchain.logger.log_agent_end(output, color="green") + final_output = output.return_values + if self.return_intermediate_steps: + final_output["intermediate_steps"] = intermediate_steps + return final_output + if self.verbose: + langchain.logger.log_agent_action(output, color="green") + # And then we lookup the tool if output.tool in name_to_tool_map: chain = name_to_tool_map[output.tool] # We then call the tool on the tool input to get an observation @@ -154,10 +206,11 @@ class Agent(Chain, BaseModel, ABC): else: observation = f"{output.tool} is not a valid tool, try another one." color = None - # We then log the observation - chained_input.add_observation( - observation, - self.observation_prefix, - self.llm_prefix, - color=color, - ) + if self.verbose: + langchain.logger.log_agent_observation( + observation, + color=color, + observation_prefix=self.agent.observation_prefix, + llm_prefix=self.agent.llm_prefix, + ) + intermediate_steps.append((output, observation)) diff --git a/langchain/agents/input.py b/langchain/agents/input.py deleted file mode 100644 index 6659ad09..00000000 --- a/langchain/agents/input.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Input manager for agents.""" -from typing import Optional - -import langchain -from langchain.schema import AgentAction - - -class ChainedInput: - """Class for working with input that is the result of chains.""" - - def __init__(self, text: str, verbose: bool = False): - """Initialize with verbose flag and initial text.""" - self._verbose = verbose - if self._verbose: - langchain.logger.log_agent_start(text) - self._input = text - - def add_action(self, action: AgentAction, color: Optional[str] = None) -> None: - """Add text to input, print if in verbose mode.""" - if self._verbose: - langchain.logger.log_agent_action(action, color=color) - self._input += action.log - - def add_observation( - self, - observation: str, - observation_prefix: str, - llm_prefix: str, - color: Optional[str], - ) -> None: - """Add observation to input, print if in verbose mode.""" - if self._verbose: - langchain.logger.log_agent_observation( - observation, - color=color, - observation_prefix=observation_prefix, - llm_prefix=llm_prefix, - ) - self._input += f"\n{observation_prefix}{observation}\n{llm_prefix}" - - @property - def input(self) -> str: - """Return the accumulated input.""" - return self._input diff --git a/langchain/agents/load_tools.py b/langchain/agents/load_tools.py new file mode 100644 index 00000000..2e41b3b9 --- /dev/null +++ b/langchain/agents/load_tools.py @@ -0,0 +1,169 @@ +# flake8: noqa +"""Load tools.""" +from typing import Any, List, Optional + +from langchain.agents.tools import Tool +from langchain.chains.api import news_docs, open_meteo_docs, tmdb_docs +from langchain.chains.api.base import APIChain +from langchain.chains.llm_math.base import LLMMathChain +from langchain.chains.pal.base import PALChain +from langchain.llms.base import BaseLLM +from langchain.python import PythonREPL +from langchain.requests import RequestsWrapper +from langchain.serpapi import SerpAPIWrapper +from langchain.utilities.bash import BashProcess + + +def _get_python_repl() -> Tool: + return Tool( + "Python REPL", + PythonREPL().run, + "A Python shell. Use this to execute python commands. Input should be a valid python command. If you expect output it should be printed out.", + ) + + +def _get_serpapi() -> Tool: + return Tool( + "Search", + SerpAPIWrapper().run, + "A search engine. Useful for when you need to answer questions about current events. Input should be a search query.", + ) + + +def _get_requests() -> Tool: + return Tool( + "Requests", + RequestsWrapper().run, + "A portal to the internet. Use this when you need to get specific content from a site. Input should be a specific url, and the output will be all the text on that page.", + ) + + +def _get_terminal() -> Tool: + return Tool( + "Terminal", + BashProcess().run, + "Executes commands in a terminal. Input should be valid commands, and the output will be any output from running that command.", + ) + + +_BASE_TOOLS = { + "python_repl": _get_python_repl, + "serpapi": _get_serpapi, + "requests": _get_requests, + "terminal": _get_terminal, +} + + +def _get_pal_math(llm: BaseLLM) -> Tool: + return Tool( + "PAL-MATH", + PALChain.from_math_prompt(llm).run, + "A language model that is really good at solving complex word math problems. Input should be a fully worded hard word math problem.", + ) + + +def _get_pal_colored_objects(llm: BaseLLM) -> Tool: + return Tool( + "PAL-COLOR-OBJ", + PALChain.from_colored_object_prompt(llm).run, + "A language model that is really good at reasoning about position and the color attributes of objects. Input should be a fully worded hard reasoning problem. Make sure to include all information about the objects AND the final question you want to answer.", + ) + + +def _get_llm_math(llm: BaseLLM) -> Tool: + return Tool( + "Calculator", + LLMMathChain(llm=llm).run, + "Useful for when you need to answer questions about math.", + ) + + +def _get_open_meteo_api(llm: BaseLLM) -> Tool: + chain = APIChain.from_llm_and_api_docs(llm, open_meteo_docs.OPEN_METEO_DOCS) + return Tool( + "Open Meteo API", + chain.run, + "Useful for when you want to get weather information from the OpenMeteo API. The input should be a question in natural language that this API can answer.", + ) + + +_LLM_TOOLS = { + "pal-math": _get_pal_math, + "pal-colored-objects": _get_pal_colored_objects, + "llm-math": _get_llm_math, + "open-meteo-api": _get_open_meteo_api, +} + + +def _get_news_api(llm: BaseLLM, **kwargs: Any) -> Tool: + news_api_key = kwargs["news_api_key"] + chain = APIChain.from_llm_and_api_docs( + llm, news_docs.NEWS_DOCS, headers={"X-Api-Key": news_api_key} + ) + return Tool( + "News API", + chain.run, + "Use this when you want to get information about the top headlines of current news stories. The input should be a question in natural language that this API can answer.", + ) + + +def _get_tmdb_api(llm: BaseLLM, **kwargs: Any) -> Tool: + tmdb_bearer_token = kwargs["tmdb_bearer_token"] + chain = APIChain.from_llm_and_api_docs( + llm, + tmdb_docs.TMDB_DOCS, + headers={"Authorization": f"Bearer {tmdb_bearer_token}"}, + ) + return Tool( + "TMDB API", + chain.run, + "Useful for when you want to get information from The Movie Database. The input should be a question in natural language that this API can answer.", + ) + + +_EXTRA_TOOLS = { + "news-api": (_get_news_api, ["news_api_key"]), + "tmdb-api": (_get_tmdb_api, ["tmdb_bearer_token"]), +} + + +def load_tools( + tool_names: List[str], llm: Optional[BaseLLM] = None, **kwargs: Any +) -> List[Tool]: + """Load tools based on their name. + + Args: + tool_names: name of tools to load. + llm: Optional language model, may be needed to initialize certain tools. + + Returns: + List of tools. + """ + tools = [] + for name in tool_names: + if name in _BASE_TOOLS: + tools.append(_BASE_TOOLS[name]()) + elif name in _LLM_TOOLS: + if llm is None: + raise ValueError(f"Tool {name} requires an LLM to be provided") + tools.append(_LLM_TOOLS[name](llm)) + elif name in _EXTRA_TOOLS: + if llm is None: + raise ValueError(f"Tool {name} requires an LLM to be provided") + _get_tool_func, extra_keys = _EXTRA_TOOLS[name] + missing_keys = set(extra_keys).difference(kwargs) + if missing_keys: + raise ValueError( + f"Tool {name} requires some parameters that were not " + f"provided: {missing_keys}" + ) + sub_kwargs = {k: kwargs[k] for k in extra_keys} + tools.append(_get_tool_func(llm=llm, **sub_kwargs)) + else: + raise ValueError(f"Got unknown tool {name}") + return tools + + +def get_all_tool_names() -> List[str]: + """Get a list of all possible tool names.""" + return list(_BASE_TOOLS) + list(_EXTRA_TOOLS) + list(_LLM_TOOLS) diff --git a/langchain/agents/loading.py b/langchain/agents/loading.py index 98b644ed..f1823b74 100644 --- a/langchain/agents/loading.py +++ b/langchain/agents/loading.py @@ -1,7 +1,7 @@ """Load agent.""" from typing import Any, List -from langchain.agents.agent import Agent +from langchain.agents.agent import AgentExecutor from langchain.agents.mrkl.base import ZeroShotAgent from langchain.agents.react.base import ReActDocstoreAgent from langchain.agents.self_ask_with_search.base import SelfAskWithSearchAgent @@ -20,7 +20,7 @@ def initialize_agent( llm: BaseLLM, agent: str = "zero-shot-react-description", **kwargs: Any, -) -> Agent: +) -> AgentExecutor: """Load agent given tools and LLM. Args: @@ -39,4 +39,5 @@ def initialize_agent( f"Valid types are: {AGENT_TO_CLASS.keys()}." ) agent_cls = AGENT_TO_CLASS[agent] - return agent_cls.from_llm_and_tools(llm, tools, **kwargs) + agent_obj = agent_cls.from_llm_and_tools(llm, tools) + return AgentExecutor.from_agent_and_tools(agent=agent_obj, tools=tools, **kwargs) diff --git a/langchain/agents/mrkl/base.py b/langchain/agents/mrkl/base.py index 98c1845f..5a502f46 100644 --- a/langchain/agents/mrkl/base.py +++ b/langchain/agents/mrkl/base.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any, Callable, List, NamedTuple, Optional, Tuple -from langchain.agents.agent import Agent +from langchain.agents.agent import Agent, AgentExecutor from langchain.agents.mrkl.prompt import FORMAT_INSTRUCTIONS, PREFIX, SUFFIX from langchain.agents.tools import Tool from langchain.llms.base import BaseLLM @@ -85,7 +85,7 @@ class ZeroShotAgent(Agent): format_instructions = FORMAT_INSTRUCTIONS.format(tool_names=tool_names) template = "\n\n".join([prefix, tool_strings, format_instructions, suffix]) if input_variables is None: - input_variables = ["input"] + input_variables = ["input", "agent_scratchpad"] return PromptTemplate(template=template, input_variables=input_variables) @classmethod @@ -101,7 +101,7 @@ class ZeroShotAgent(Agent): return get_action_and_input(text) -class MRKLChain(ZeroShotAgent): +class MRKLChain(AgentExecutor): """Chain that implements the MRKL system. Example: @@ -118,7 +118,7 @@ class MRKLChain(ZeroShotAgent): @classmethod def from_chains( cls, llm: BaseLLM, chains: List[ChainConfig], **kwargs: Any - ) -> Agent: + ) -> AgentExecutor: """User friendly way to initialize the MRKL chain. This is intended to be an easy way to get up and running with the @@ -158,4 +158,5 @@ class MRKLChain(ZeroShotAgent): Tool(name=c.action_name, func=c.action, description=c.action_description) for c in chains ] - return cls.from_llm_and_tools(llm, tools, **kwargs) + agent = ZeroShotAgent.from_llm_and_tools(llm, tools) + return cls(agent=agent, tools=tools, **kwargs) diff --git a/langchain/agents/mrkl/prompt.py b/langchain/agents/mrkl/prompt.py index c32310d4..db6827b5 100644 --- a/langchain/agents/mrkl/prompt.py +++ b/langchain/agents/mrkl/prompt.py @@ -12,4 +12,5 @@ Thought: I now know the final answer Final Answer: the final answer to the original input question""" SUFFIX = """Begin! -Question: {input}""" +Question: {input} +Thought:{agent_scratchpad}""" diff --git a/langchain/agents/react/base.py b/langchain/agents/react/base.py index 41cb9f02..c5c6f39f 100644 --- a/langchain/agents/react/base.py +++ b/langchain/agents/react/base.py @@ -1,14 +1,13 @@ """Chain that implements the ReAct paper from https://arxiv.org/pdf/2210.03629.pdf.""" import re -from typing import Any, ClassVar, List, Optional, Tuple +from typing import Any, List, Optional, Tuple from pydantic import BaseModel -from langchain.agents.agent import Agent +from langchain.agents.agent import Agent, AgentExecutor from langchain.agents.react.textworld_prompt import TEXTWORLD_PROMPT from langchain.agents.react.wiki_prompt import WIKI_PROMPT from langchain.agents.tools import Tool -from langchain.chains.llm import LLMChain from langchain.docstore.base import Docstore from langchain.docstore.document import Document from langchain.llms.base import BaseLLM @@ -18,7 +17,10 @@ from langchain.prompts.base import BasePromptTemplate class ReActDocstoreAgent(Agent, BaseModel): """Agent for the ReAct chin.""" - prompt: ClassVar[BasePromptTemplate] = WIKI_PROMPT + @classmethod + def create_prompt(cls, tools: List[Tool]) -> BasePromptTemplate: + """Return default prompt.""" + return WIKI_PROMPT i: int = 1 @@ -64,7 +66,7 @@ class ReActDocstoreAgent(Agent, BaseModel): @property def _stop(self) -> List[str]: - return [f"\nObservation {self.i}: "] + return [f"\nObservation {self.i}:"] @property def llm_prefix(self) -> str: @@ -100,9 +102,10 @@ class DocstoreExplorer: class ReActTextWorldAgent(ReActDocstoreAgent, BaseModel): """Agent for the ReAct TextWorld chain.""" - prompt: ClassVar[BasePromptTemplate] = TEXTWORLD_PROMPT - - i: int = 1 + @classmethod + def create_prompt(cls, tools: List[Tool]) -> BasePromptTemplate: + """Return default prompt.""" + return TEXTWORLD_PROMPT @classmethod def _validate_tools(cls, tools: List[Tool]) -> None: @@ -113,7 +116,7 @@ class ReActTextWorldAgent(ReActDocstoreAgent, BaseModel): raise ValueError(f"Tool name should be Play, got {tool_names}") -class ReActChain(ReActDocstoreAgent): +class ReActChain(AgentExecutor): """Chain that implements the ReAct paper. Example: @@ -130,5 +133,5 @@ class ReActChain(ReActDocstoreAgent): Tool(name="Search", func=docstore_explorer.search), Tool(name="Lookup", func=docstore_explorer.lookup), ] - llm_chain = LLMChain(llm=llm, prompt=WIKI_PROMPT) - super().__init__(llm_chain=llm_chain, tools=tools, **kwargs) + agent = ReActDocstoreAgent.from_llm_and_tools(llm, tools) + super().__init__(agent=agent, tools=tools, **kwargs) diff --git a/langchain/agents/react/textworld_prompt.py b/langchain/agents/react/textworld_prompt.py index d8bb31e8..b832a6bb 100644 --- a/langchain/agents/react/textworld_prompt.py +++ b/langchain/agents/react/textworld_prompt.py @@ -44,6 +44,9 @@ Action 4: Finish[yes] """ ] -SUFFIX = """\n\nSetup: {input}""" +SUFFIX = """\n\nSetup: {input} +{agent_scratchpad}""" -TEXTWORLD_PROMPT = PromptTemplate.from_examples(EXAMPLES, SUFFIX, ["input"]) +TEXTWORLD_PROMPT = PromptTemplate.from_examples( + EXAMPLES, SUFFIX, ["input", "agent_scratchpad"] +) diff --git a/langchain/agents/react/wiki_prompt.py b/langchain/agents/react/wiki_prompt.py index 27f7565e..24370406 100644 --- a/langchain/agents/react/wiki_prompt.py +++ b/langchain/agents/react/wiki_prompt.py @@ -107,6 +107,9 @@ Thought 3: Leonid Levin is a mathematician and computer scientist. So Pavel Urys and Leonid Levin have the same type of work. Action 3: Finish[yes]""", ] -SUFFIX = """\n\nQuestion: {input}""" +SUFFIX = """\n\nQuestion: {input} +{agent_scratchpad}""" -WIKI_PROMPT = PromptTemplate.from_examples(EXAMPLES, SUFFIX, ["input"]) +WIKI_PROMPT = PromptTemplate.from_examples( + EXAMPLES, SUFFIX, ["input", "agent_scratchpad"] +) diff --git a/langchain/agents/self_ask_with_search/base.py b/langchain/agents/self_ask_with_search/base.py index 0ac8573d..4432c76f 100644 --- a/langchain/agents/self_ask_with_search/base.py +++ b/langchain/agents/self_ask_with_search/base.py @@ -1,10 +1,9 @@ """Chain that does self ask with search.""" -from typing import Any, ClassVar, List, Optional, Tuple +from typing import Any, List, Optional, Tuple -from langchain.agents.agent import Agent +from langchain.agents.agent import Agent, AgentExecutor from langchain.agents.self_ask_with_search.prompt import PROMPT from langchain.agents.tools import Tool -from langchain.chains.llm import LLMChain from langchain.llms.base import BaseLLM from langchain.prompts.base import BasePromptTemplate from langchain.serpapi import SerpAPIWrapper @@ -13,7 +12,10 @@ from langchain.serpapi import SerpAPIWrapper class SelfAskWithSearchAgent(Agent): """Agent for the self-ask-with-search paper.""" - prompt: ClassVar[BasePromptTemplate] = PROMPT + @classmethod + def create_prompt(cls, tools: List[Tool]) -> BasePromptTemplate: + """Prompt does not depend on tools.""" + return PROMPT @classmethod def _validate_tools(cls, tools: List[Tool]) -> None: @@ -58,10 +60,10 @@ class SelfAskWithSearchAgent(Agent): @property def starter_string(self) -> str: """Put this string after user input but before first LLM call.""" - return "\nAre follow up questions needed here:" + return "Are follow up questions needed here:" -class SelfAskWithSearchChain(SelfAskWithSearchAgent): +class SelfAskWithSearchChain(AgentExecutor): """Chain that does self ask with search. Example: @@ -75,5 +77,5 @@ class SelfAskWithSearchChain(SelfAskWithSearchAgent): def __init__(self, llm: BaseLLM, search_chain: SerpAPIWrapper, **kwargs: Any): """Initialize with just an LLM and a search chain.""" search_tool = Tool(name="Intermediate Answer", func=search_chain.run) - llm_chain = LLMChain(llm=llm, prompt=PROMPT) - super().__init__(llm_chain=llm_chain, tools=[search_tool], **kwargs) + agent = SelfAskWithSearchAgent.from_llm_and_tools(llm, [search_tool]) + super().__init__(agent=agent, tools=[search_tool], **kwargs) diff --git a/langchain/agents/self_ask_with_search/prompt.py b/langchain/agents/self_ask_with_search/prompt.py index 0a387457..c82de28d 100644 --- a/langchain/agents/self_ask_with_search/prompt.py +++ b/langchain/agents/self_ask_with_search/prompt.py @@ -37,5 +37,8 @@ Follow up: Where is Martin Campbell from? Intermediate answer: New Zealand. So the final answer is: No -Question: {input}""" -PROMPT = PromptTemplate(input_variables=["input"], template=_DEFAULT_TEMPLATE) +Question: {input} +Are followup questions needed here:{agent_scratchpad}""" +PROMPT = PromptTemplate( + input_variables=["input", "agent_scratchpad"], template=_DEFAULT_TEMPLATE +) diff --git a/langchain/chains/api/news_docs.py b/langchain/chains/api/news_docs.py new file mode 100644 index 00000000..7e84c1da --- /dev/null +++ b/langchain/chains/api/news_docs.py @@ -0,0 +1,32 @@ +# flake8: noqa +NEWS_DOCS = """API documentation: +Endpoint: https://newsapi.org +Top headlines /v2/top-headlines + +This endpoint provides live top and breaking headlines for a country, specific category in a country, single source, or multiple sources. You can also search with keywords. Articles are sorted by the earliest date published first. + +This endpoint is great for retrieving headlines for use with news tickers or similar. +Request parameters + + country | The 2-letter ISO 3166-1 code of the country you want to get headlines for. Possible options: ae ar at au be bg br ca ch cn co cu cz de eg fr gb gr hk hu id ie il in it jp kr lt lv ma mx my ng nl no nz ph pl pt ro rs ru sa se sg si sk th tr tw ua us ve za. Note: you can't mix this param with the sources param. + category | The category you want to get headlines for. Possible options: business entertainment general health science sports technology. Note: you can't mix this param with the sources param. + sources | A comma-seperated string of identifiers for the news sources or blogs you want headlines from. Use the /top-headlines/sources endpoint to locate these programmatically or look at the sources index. Note: you can't mix this param with the country or category params. + q | Keywords or a phrase to search for. + pageSize | int | The number of results to return per page (request). 20 is the default, 100 is the maximum. + page | int | Use this to page through the results if the total results found is greater than the page size. + +Response object + status | string | If the request was successful or not. Options: ok, error. In the case of error a code and message property will be populated. + totalResults | int | The total number of results available for your request. + articles | array[article] | The results of the request. + source | object | The identifier id and a display name name for the source this article came from. + author | string | The author of the article + title | string | The headline or title of the article. + description | string | A description or snippet from the article. + url | string | The direct URL to the article. + urlToImage | string | The URL to a relevant image for the article. + publishedAt | string | The date and time that the article was published, in UTC (+000) + content | string | The unformatted content of the article, where available. This is truncated to 200 chars. + +Use page size: 2 +""" diff --git a/langchain/chains/api/open_meteo_docs.py b/langchain/chains/api/open_meteo_docs.py new file mode 100644 index 00000000..4abd86fb --- /dev/null +++ b/langchain/chains/api/open_meteo_docs.py @@ -0,0 +1,33 @@ +# flake8: noqa +OPEN_METEO_DOCS = """BASE URL: https://api.open-meteo.com/ + +API Documentation +The API endpoint /v1/forecast accepts a geographical coordinate, a list of weather variables and responds with a JSON hourly weather forecast for 7 days. Time always starts at 0:00 today and contains 168 hours. All URL parameters are listed below: + +Parameter Format Required Default Description +latitude, longitude Floating point Yes Geographical WGS84 coordinate of the location +hourly String array No A list of weather variables which should be returned. Values can be comma separated, or multiple &hourly= parameter in the URL can be used. +daily String array No A list of daily weather variable aggregations which should be returned. Values can be comma separated, or multiple &daily= parameter in the URL can be used. If daily weather variables are specified, parameter timezone is required. +current_weather Bool No false Include current weather conditions in the JSON output. +temperature_unit String No celsius If fahrenheit is set, all temperature values are converted to Fahrenheit. +windspeed_unit String No kmh Other wind speed speed units: ms, mph and kn +precipitation_unit String No mm Other precipitation amount units: inch +timeformat String No iso8601 If format unixtime is selected, all time values are returned in UNIX epoch time in seconds. Please note that all timestamp are in GMT+0! For daily values with unix timestamps, please apply utc_offset_seconds again to get the correct date. +timezone String No GMT If timezone is set, all timestamps are returned as local-time and data is returned starting at 00:00 local-time. Any time zone name from the time zone database is supported. If auto is set as a time zone, the coordinates will be automatically resolved to the local time zone. +past_days Integer (0-2) No 0 If past_days is set, yesterday or the day before yesterday data are also returned. +start_date +end_date String (yyyy-mm-dd) No The time interval to get weather data. A day must be specified as an ISO8601 date (e.g. 2022-06-30). +models String array No auto Manually select one or more weather models. Per default, the best suitable weather models will be combined. + +Hourly Parameter Definition +The parameter &hourly= accepts the following values. Most weather variables are given as an instantaneous value for the indicated hour. Some variables like precipitation are calculated from the preceding hour as an average or sum. + +Variable Valid time Unit Description +temperature_2m Instant °C (°F) Air temperature at 2 meters above ground +snowfall Preceding hour sum cm (inch) Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent +rain Preceding hour sum mm (inch) Rain from large scale weather systems of the preceding hour in millimeter +showers Preceding hour sum mm (inch) Showers from convective precipitation in millimeters from the preceding hour +weathercode Instant WMO code Weather condition as a numeric code. Follow WMO weather interpretation codes. See table below for details. +snow_depth Instant meters Snow depth on the ground +freezinglevel_height Instant meters Altitude above sea level of the 0°C level +visibility Instant meters Viewing distance in meters. Influenced by low clouds, humidity and aerosols. Maximum visibility is approximately 24 km.""" diff --git a/langchain/chains/api/tmdb_docs.py b/langchain/chains/api/tmdb_docs.py new file mode 100644 index 00000000..20596f0c --- /dev/null +++ b/langchain/chains/api/tmdb_docs.py @@ -0,0 +1,37 @@ +# flake8: noqa +TMDB_DOCS = """API documentation: +Endpoint: https://api.themoviedb.org/3 +GET /search/movie + +This API is for searching movies. + +Query parameters table: +language | string | Pass a ISO 639-1 value to display translated data for the fields that support it. minLength: 2, pattern: ([a-z]{2})-([A-Z]{2}), default: en-US | optional +query | string | Pass a text query to search. This value should be URI encoded. minLength: 1 | required +page | integer | Specify which page to query. minimum: 1, maximum: 1000, default: 1 | optional +include_adult | boolean | Choose whether to inlcude adult (pornography) content in the results. default | optional +region | string | Specify a ISO 3166-1 code to filter release dates. Must be uppercase. pattern: ^[A-Z]{2}$ | optional +year | integer | optional +primary_release_year | integer | optional + +Response schema (JSON object): +page | integer | optional +total_results | integer | optional +total_pages | integer | optional +results | array[object] (Movie List Result Object) + +Each object in the "results" key has the following schema: +poster_path | string or null | optional +adult | boolean | optional +overview | string | optional +release_date | string | optional +genre_ids | array[integer] | optional +id | integer | optional +original_title | string | optional +original_language | string | optional +title | string | optional +backdrop_path | string or null | optional +popularity | number | optional +vote_count | integer | optional +video | boolean | optional +vote_average | number | optional""" diff --git a/langchain/chains/base.py b/langchain/chains/base.py index ececc8f0..123794a3 100644 --- a/langchain/chains/base.py +++ b/langchain/chains/base.py @@ -75,7 +75,7 @@ class Chain(BaseModel, ABC): def __call__( self, inputs: Union[Dict[str, Any], Any], return_only_outputs: bool = False - ) -> Dict[str, str]: + ) -> Dict[str, Any]: """Run the logic of this chain and add to output if desired. Args: diff --git a/langchain/logger.py b/langchain/logger.py index 27b8fd84..d264da90 100644 --- a/langchain/logger.py +++ b/langchain/logger.py @@ -2,7 +2,7 @@ from typing import Any, Optional from langchain.input import print_text -from langchain.schema import AgentAction +from langchain.schema import AgentAction, AgentFinish class BaseLogger: @@ -12,7 +12,7 @@ class BaseLogger: """Log the start of an agent interaction.""" pass - def log_agent_end(self, text: str, **kwargs: Any) -> None: + def log_agent_end(self, finish: AgentFinish, **kwargs: Any) -> None: """Log the end of an agent interaction.""" pass @@ -63,3 +63,9 @@ class StdOutLogger(BaseLogger): """Print the prompt in green.""" print("Prompt after formatting:") print_text(prompt, color="green", end="\n") + + def log_agent_end( + self, finish: AgentFinish, color: Optional[str] = None, **kwargs: Any + ) -> None: + """Log the end of an agent interaction.""" + print_text(finish.log, color=color) diff --git a/langchain/python.py b/langchain/python.py index 88ad4db0..f31decbe 100644 --- a/langchain/python.py +++ b/langchain/python.py @@ -16,7 +16,11 @@ class PythonREPL: """Run command with own globals/locals and returns anything printed.""" old_stdout = sys.stdout sys.stdout = mystdout = StringIO() - exec(command, self._globals, self._locals) - sys.stdout = old_stdout - output = mystdout.getvalue() + try: + exec(command, self._globals, self._locals) + sys.stdout = old_stdout + output = mystdout.getvalue() + except Exception as e: + sys.stdout = old_stdout + output = str(e) return output diff --git a/langchain/schema.py b/langchain/schema.py index 4e255e3e..31cf1cdc 100644 --- a/langchain/schema.py +++ b/langchain/schema.py @@ -11,6 +11,13 @@ class AgentAction(NamedTuple): log: str +class AgentFinish(NamedTuple): + """Agent's return value.""" + + return_values: dict + log: str + + class Generation(NamedTuple): """Output of a single generation.""" diff --git a/langchain/utilities/bash.py b/langchain/utilities/bash.py index afc0e34f..6872430a 100644 --- a/langchain/utilities/bash.py +++ b/langchain/utilities/bash.py @@ -1,6 +1,6 @@ """Wrapper around subprocess to run commands.""" import subprocess -from typing import List +from typing import List, Union class BashProcess: @@ -10,9 +10,11 @@ class BashProcess: """Initialize with stripping newlines.""" self.strip_newlines = strip_newlines - def run(self, commands: List[str]) -> str: + def run(self, commands: Union[str, List[str]]) -> str: """Run commands and return final output.""" outputs = [] + if isinstance(commands, str): + commands = [commands] for command in commands: try: output = subprocess.check_output(command, shell=True).decode() diff --git a/tests/integration_tests/chains/test_react.py b/tests/integration_tests/chains/test_react.py index 1c648705..76a93609 100644 --- a/tests/integration_tests/chains/test_react.py +++ b/tests/integration_tests/chains/test_react.py @@ -7,7 +7,7 @@ from langchain.llms.openai import OpenAI def test_react() -> None: """Test functionality on a prompt.""" - llm = OpenAI(temperature=0) + llm = OpenAI(temperature=0, model_name="text-davinci-002") react = ReActChain(llm=llm, docstore=Wikipedia()) question = ( "Author David Chanoff has collaborated with a U.S. Navy admiral " diff --git a/tests/integration_tests/chains/test_self_ask_with_search.py b/tests/integration_tests/chains/test_self_ask_with_search.py index e4536f75..3463ec9e 100644 --- a/tests/integration_tests/chains/test_self_ask_with_search.py +++ b/tests/integration_tests/chains/test_self_ask_with_search.py @@ -15,4 +15,4 @@ def test_self_ask_with_search() -> None: ) answer = chain.run(question) final_answer = answer.split("\n")[-1] - assert final_answer == "So the final answer is: El Palmar, Murcia, Spain" + assert final_answer == "El Palmar, Spain" diff --git a/tests/unit_tests/agents/test_react.py b/tests/unit_tests/agents/test_react.py index f3dc8da5..05c6298a 100644 --- a/tests/unit_tests/agents/test_react.py +++ b/tests/unit_tests/agents/test_react.py @@ -10,6 +10,7 @@ from langchain.docstore.base import Docstore from langchain.docstore.document import Document from langchain.llms.base import LLM from langchain.prompts.prompt import PromptTemplate +from langchain.schema import AgentAction _PAGE_CONTENT = """This is a page about LangChain. @@ -61,10 +62,9 @@ def test_predict_until_observation_normal() -> None: Tool("Lookup", lambda x: x), ] agent = ReActDocstoreAgent.from_llm_and_tools(fake_llm, tools) - output = agent.get_action("") - assert output.log == outputs[0] - assert output.tool == "Search" - assert output.tool_input == "foo" + output = agent.plan([], input="") + expected_output = AgentAction("Search", "foo", outputs[0]) + assert output == expected_output def test_predict_until_observation_repeat() -> None: @@ -76,10 +76,9 @@ def test_predict_until_observation_repeat() -> None: Tool("Lookup", lambda x: x), ] agent = ReActDocstoreAgent.from_llm_and_tools(fake_llm, tools) - output = agent.get_action("") - assert output.log == "foo\nAction 1: Search[foo]" - assert output.tool == "Search" - assert output.tool_input == "foo" + output = agent.plan([], input="") + expected_output = AgentAction("Search", "foo", "foo\nAction 1: Search[foo]") + assert output == expected_output def test_react_chain() -> None: diff --git a/tests/unit_tests/test_input.py b/tests/unit_tests/test_input.py deleted file mode 100644 index 5d26535f..00000000 --- a/tests/unit_tests/test_input.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Test input manipulating logic.""" - -import sys -from io import StringIO - -from langchain.agents.input import ChainedInput -from langchain.input import get_color_mapping - - -def test_chained_input_not_verbose() -> None: - """Test chained input logic.""" - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - chained_input = ChainedInput("foo") - sys.stdout = old_stdout - output = mystdout.getvalue() - assert output == "" - assert chained_input.input == "foo" - - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - chained_input.add_observation("bar", "1", "2", None) - sys.stdout = old_stdout - output = mystdout.getvalue() - assert output == "" - assert chained_input.input == "foo\n1bar\n2" - - -def test_chained_input_verbose() -> None: - """Test chained input logic, making sure verbose doesn't mess it up.""" - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - chained_input = ChainedInput("foo", verbose=True) - sys.stdout = old_stdout - output = mystdout.getvalue() - assert output == "foo" - assert chained_input.input == "foo" - - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - chained_input.add_observation("bar", "1", "2", None) - sys.stdout = old_stdout - output = mystdout.getvalue() - assert output == "\n1bar\n2" - assert chained_input.input == "foo\n1bar\n2" - - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() - chained_input.add_observation("baz", "3", "4", "blue") - sys.stdout = old_stdout - output = mystdout.getvalue() - assert output == "\n3\x1b[36;1m\x1b[1;3mbaz\x1b[0m\n4" - assert chained_input.input == "foo\n1bar\n2\n3baz\n4" - - -def test_get_color_mapping() -> None: - """Test getting of color mapping.""" - # Test on few inputs. - items = ["foo", "bar"] - output = get_color_mapping(items) - expected_output = {"foo": "blue", "bar": "yellow"} - assert output == expected_output - - # Test on a lot of inputs. - items = [f"foo-{i}" for i in range(20)] - output = get_color_mapping(items) - assert len(output) == 20 - - -def test_get_color_mapping_excluded_colors() -> None: - """Test getting of color mapping with excluded colors.""" - items = ["foo", "bar"] - output = get_color_mapping(items, excluded_colors=["blue"]) - expected_output = {"foo": "yellow", "bar": "pink"} - assert output == expected_output diff --git a/tests/unit_tests/test_python.py b/tests/unit_tests/test_python.py index 3cdd4dc4..5d5461d3 100644 --- a/tests/unit_tests/test_python.py +++ b/tests/unit_tests/test_python.py @@ -1,7 +1,5 @@ """Test functionality of Python REPL.""" -import pytest - from langchain.python import PythonREPL @@ -22,8 +20,8 @@ def test_python_repl_no_previous_variables() -> None: """Test that it does not have access to variables created outside the scope.""" foo = 3 # noqa: F841 repl = PythonREPL() - with pytest.raises(NameError): - repl.run("print(foo)") + output = repl.run("print(foo)") + assert output == "name 'foo' is not defined" def test_python_repl_pass_in_locals() -> None: