From 6b8d144ccca4668d962cbf3986db377e9ec24854 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 9 May 2023 21:07:56 -0700 Subject: [PATCH] Harrison/plan and solve (#4422) --- docs/modules/agents.rst | 39 +- .../agents/agents/custom_llm_chat_agent.ipynb | 49 +-- docs/modules/agents/plan_and_solve.ipynb | 363 ++++++++++++++++++ langchain/agents/__init__.py | 8 + langchain/agents/plan_and_execute/__init__.py | 7 + .../agents/plan_and_execute/agent_executor.py | 56 +++ .../plan_and_execute/executors/__init__.py | 0 .../executors/agent_executor.py | 28 ++ .../agents/plan_and_execute/executors/base.py | 40 ++ .../plan_and_execute/planners/__init__.py | 0 .../agents/plan_and_execute/planners/base.py | 40 ++ .../plan_and_execute/planners/chat_planner.py | 42 ++ langchain/agents/plan_and_execute/schema.py | 47 +++ langchain/agents/structured_chat/base.py | 7 +- tests/unit_tests/agents/test_public_api.py | 5 +- 15 files changed, 701 insertions(+), 30 deletions(-) create mode 100644 docs/modules/agents/plan_and_solve.ipynb create mode 100644 langchain/agents/plan_and_execute/__init__.py create mode 100644 langchain/agents/plan_and_execute/agent_executor.py create mode 100644 langchain/agents/plan_and_execute/executors/__init__.py create mode 100644 langchain/agents/plan_and_execute/executors/agent_executor.py create mode 100644 langchain/agents/plan_and_execute/executors/base.py create mode 100644 langchain/agents/plan_and_execute/planners/__init__.py create mode 100644 langchain/agents/plan_and_execute/planners/base.py create mode 100644 langchain/agents/plan_and_execute/planners/chat_planner.py create mode 100644 langchain/agents/plan_and_execute/schema.py diff --git a/docs/modules/agents.rst b/docs/modules/agents.rst index e90a671d..36ce8841 100644 --- a/docs/modules/agents.rst +++ b/docs/modules/agents.rst @@ -10,12 +10,24 @@ but potentially an unknown chain that depends on the user's input. In these types of chains, there is a “agent” which has access to a suite of tools. Depending on the user input, the agent can then decide which, if any, of these tools to call. +At the moment, there are two main types of agents: + +1. "Action Agents": these agents decide an action to take and take that action one step at a time +2. "Plan-and-Execute Agents": these agents first decide a plan of actions to take, and then execute those actions one at a time. + +When should you use each one? Action Agents are more conventional, and good for small tasks. +For more complex or long running tasks, the initial planning step helps to maintain long term objectives and focus. However, that comes at the expense of generally more calls and higher latency. +These two agents are also not mutually exclusive - in fact, it is often best to have an Action Agent be in change of the execution for the Plan and Execute agent. + +Action Agents +------------- + High level pseudocode of agents looks something like: - Some user input is received - The `agent` decides which `tool` - if any - to use, and what the input to that tool should be -- That `tool` is then called with that `tool input`, and an `observation` is recorded (this is just the output of calling that tool with that tool input. -- That history of `tool`, `tool input`, and `observation` is passed back into the `agent`, and it decides what steps to take next +- That `tool` is then called with that `tool input`, and an `observation` is recorded (this is just the output of calling that tool with that tool input) +- That history of `tool`, `tool input`, and `observation` is passed back into the `agent`, and it decides what step to take next - This is repeated until the `agent` decides it no longer needs to use a `tool`, and then it responds directly to the user. The different abstractions involved in agents are as follows: @@ -69,8 +81,7 @@ In this section we go over the Agent Executor class, which is responsible for ca the agent and tools in a loop. We go over different ways to customize this, and options you can use for more control. -Go Deeper ---------- +**Go Deeper** .. toctree:: :maxdepth: 1 @@ -79,3 +90,23 @@ Go Deeper ./agents/agents.rst ./agents/toolkits.rst ./agents/agent_executors.rst + +Plan-and-Execute Agents +----------------------- + +High level pseudocode of agents looks something like: + +- Some user input is received +- The planner lists out the steps to take +- The executor goes through the list of steps, executing them + +The most typical implementation is to have the planner be a language model, +and the executor be an action agent. + +**Go Deeper** + +.. toctree:: + :maxdepth: 1 + + ./agents/plan_and_execute.ipynb + diff --git a/docs/modules/agents/agents/custom_llm_chat_agent.ipynb b/docs/modules/agents/agents/custom_llm_chat_agent.ipynb index 571c928d..cdc54a21 100644 --- a/docs/modules/agents/agents/custom_llm_chat_agent.ipynb +++ b/docs/modules/agents/agents/custom_llm_chat_agent.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "9af9734e", "metadata": {}, "outputs": [], @@ -100,13 +100,13 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "id": "339b1bb8", "metadata": {}, "outputs": [], "source": [ "# Set up the base template\n", - "template = \"\"\"Answer the following questions as best you can, but speaking as a pirate might speak. You have access to the following tools:\n", + "template = \"\"\"Complete the objective as best you can. You have access to the following tools:\n", "\n", "{tools}\n", "\n", @@ -121,7 +121,11 @@ "Thought: I now know the final answer\n", "Final Answer: the final answer to the original input question\n", "\n", - "Begin! Remember to speak as a pirate when giving your final answer. Use lots of \"Arg\"s\n", + "These were previous tasks you completed:\n", + "\n", + "\n", + "\n", + "Begin!\n", "\n", "Question: {input}\n", "{agent_scratchpad}\"\"\"" @@ -129,7 +133,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 13, "id": "fd969d31", "metadata": {}, "outputs": [], @@ -161,7 +165,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 14, "id": "798ef9fb", "metadata": {}, "outputs": [], @@ -189,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 15, "id": "7c6fe0d3", "metadata": {}, "outputs": [], @@ -218,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 16, "id": "d278706a", "metadata": {}, "outputs": [], @@ -238,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 17, "id": "f9d4c374", "metadata": {}, "outputs": [], @@ -270,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 18, "id": "9b1cc2a2", "metadata": {}, "outputs": [], @@ -281,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 19, "id": "e4f5092f", "metadata": {}, "outputs": [], @@ -307,7 +311,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 20, "id": "490604e9", "metadata": {}, "outputs": [], @@ -317,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 21, "id": "653b1617", "metadata": {}, "outputs": [ @@ -328,16 +332,13 @@ "\n", "\n", "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3mThought: Wot year be it now? That be important to know the answer.\n", - "Action: Search\n", - "Action Input: \"current population canada 2023\"\u001b[0m\n", - "\n", - "Observation:\u001b[36;1m\u001b[1;3m38,649,283\u001b[0m\u001b[32;1m\u001b[1;3mAhoy! That be the correct year, but the answer be in regular numbers. 'Tis time to translate to pirate speak.\n", + "\u001b[32;1m\u001b[1;3mThought: I should use a reliable search engine to get accurate information.\n", "Action: Search\n", - "Action Input: \"38,649,283 in pirate speak\"\u001b[0m\n", + "Action Input: \"Leo DiCaprio girlfriend\"\u001b[0m\n", "\n", - "Observation:\u001b[36;1m\u001b[1;3mBrush up on your “Pirate Talk” with these helpful pirate phrases. Aaaarrrrgggghhhh! Pirate catch phrase of grumbling or disgust. Ahoy! Hello! Ahoy, Matey, Hello ...\u001b[0m\u001b[32;1m\u001b[1;3mThat be not helpful, I'll just do the translation meself.\n", - "Final Answer: Arrrr, thar be 38,649,283 scallywags in Canada as of 2023.\u001b[0m\n", + "Observation:\u001b[36;1m\u001b[1;3mHe went on to date Gisele Bündchen, Bar Refaeli, Blake Lively, Toni Garrn and Nina Agdal, among others, before finally settling down with current girlfriend Camila Morrone, who is 23 years his junior.\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mI have found the answer to the question.\n", + "Final Answer: Leo DiCaprio's current girlfriend is Camila Morrone.\u001b[0m\n", "\n", "\u001b[1m> Finished chain.\u001b[0m\n" ] @@ -345,16 +346,16 @@ { "data": { "text/plain": [ - "'Arrrr, thar be 38,649,283 scallywags in Canada as of 2023.'" + "\"Leo DiCaprio's current girlfriend is Camila Morrone.\"" ] }, - "execution_count": 16, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "agent_executor.run(\"How many people live in canada as of 2023?\")" + "agent_executor.run(\"Search for Leo DiCaprio's girlfriend on the internet.\")" ] }, { diff --git a/docs/modules/agents/plan_and_solve.ipynb b/docs/modules/agents/plan_and_solve.ipynb new file mode 100644 index 00000000..93904eef --- /dev/null +++ b/docs/modules/agents/plan_and_solve.ipynb @@ -0,0 +1,363 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "406483c4", + "metadata": {}, + "source": [ + "## Plan and Execute\n", + "\n", + "Plan and execute agents accomplish an objective by first planning what to do, then executing the sub tasks. This idea is largely inspired by [BabyAGI](https://github.com/yoheinakajima/babyagi) and then the [\"Plan-and-Solve\" paper](https://arxiv.org/abs/2305.04091).\n", + "\n", + "The planning is almost always done by an LLM.\n", + "\n", + "The execution is usually done by a separate agent (equipped with tools)." + ] + }, + { + "cell_type": "markdown", + "id": "91192118", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6ccd1dc5", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.chat_models import ChatOpenAI\n", + "from langchain.llms import OpenAI\n", + "from langchain.agents import PlanAndExecute, load_agent_executor, load_chat_planner\n", + "from langchain import SerpAPIWrapper\n", + "from langchain.agents.tools import Tool\n", + "from langchain import LLMMathChain" + ] + }, + { + "cell_type": "markdown", + "id": "0b10d200", + "metadata": {}, + "source": [ + "## Tools" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3c00f724", + "metadata": {}, + "outputs": [], + "source": [ + "search = SerpAPIWrapper()\n", + "llm = OpenAI(temperature=0)\n", + "search = SerpAPIWrapper()\n", + "llm_math_chain = LLMMathChain.from_llm(llm=llm, verbose=True)\n", + "tools = [\n", + " Tool(\n", + " name = \"Search\",\n", + " func=search.run,\n", + " description=\"useful for when you need to answer questions about current events\"\n", + " ),\n", + " Tool(\n", + " name=\"Calculator\",\n", + " func=llm_math_chain.run,\n", + " description=\"useful for when you need to answer questions about math\"\n", + " ),\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "ce38ae84", + "metadata": {}, + "source": [ + "## Planner, Executor, and Agent" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0ab2cadd", + "metadata": {}, + "outputs": [], + "source": [ + "model = ChatOpenAI(temperature=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7b2419f2", + "metadata": {}, + "outputs": [], + "source": [ + "planner = load_chat_planner(model)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ed9f518b", + "metadata": {}, + "outputs": [], + "source": [ + "executor = load_agent_executor(model, tools, verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "36943178", + "metadata": {}, + "outputs": [], + "source": [ + "agent = PlanAndExecute(planner=planner, executer=executor, verbose=True)" + ] + }, + { + "cell_type": "markdown", + "id": "8be9f1bd", + "metadata": {}, + "source": [ + "## Run Example" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4891062e", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001b[1m> Entering new PlanAndExecute chain...\u001b[0m\n", + "steps=[Step(value=\"Search for Leo DiCaprio's girlfriend on the internet.\"), Step(value='Find her current age.'), Step(value='Raise her current age to the 0.43 power using a calculator or programming language.'), Step(value='Output the result.'), Step(value=\"Given the above steps taken, respond to the user's original question.\\n\\n\")]\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mAction:\n", + "```\n", + "{\n", + " \"action\": \"Search\",\n", + " \"action_input\": \"Who is Leo DiCaprio's girlfriend?\"\n", + "}\n", + "``` \n", + "\n", + "\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3mDiCaprio 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. The power couple were first supposedly an item in September after being spotted getting cozy during a party at New York Fashion Week.\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mBased on the previous observation, I can provide the answer to the current objective. \n", + "Action:\n", + "```\n", + "{\n", + " \"action\": \"Final Answer\",\n", + " \"action_input\": \"Leo DiCaprio is currently linked to Gigi Hadid.\"\n", + "}\n", + "```\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "*****\n", + "\n", + "Step: Search for Leo DiCaprio's girlfriend on the internet.\n", + "\n", + "Response: Leo DiCaprio is currently linked to Gigi Hadid.\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mAction:\n", + "```\n", + "{\n", + " \"action\": \"Search\",\n", + " \"action_input\": \"What is Gigi Hadid's current age?\"\n", + "}\n", + "```\n", + "\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3m28 years\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mPrevious steps: steps=[(Step(value=\"Search for Leo DiCaprio's girlfriend on the internet.\"), StepResponse(response='Leo DiCaprio is currently linked to Gigi Hadid.'))]\n", + "\n", + "Current objective: value='Find her current age.'\n", + "\n", + "Action:\n", + "```\n", + "{\n", + " \"action\": \"Search\",\n", + " \"action_input\": \"What is Gigi Hadid's current age?\"\n", + "}\n", + "```\n", + "\n", + "\u001b[0m\n", + "Observation: \u001b[36;1m\u001b[1;3m28 years\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mBased on my search, Gigi Hadid's current age is 26 years old. \n", + "Action:\n", + "```\n", + "{\n", + " \"action\": \"Final Answer\",\n", + " \"action_input\": \"Gigi Hadid's current age is 26 years old.\"\n", + "}\n", + "```\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "*****\n", + "\n", + "Step: Find her current age.\n", + "\n", + "Response: Gigi Hadid's current age is 26 years old.\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mAction:\n", + "```\n", + "{\n", + " \"action\": \"Calculator\",\n", + " \"action_input\": \"26 ** 0.43\"\n", + "}\n", + "```\n", + "\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", + "26 ** 0.43\u001b[32;1m\u001b[1;3m\n", + "```text\n", + "26 ** 0.43\n", + "```\n", + "...numexpr.evaluate(\"26 ** 0.43\")...\n", + "\u001b[0m\n", + "Answer: \u001b[33;1m\u001b[1;3m4.059182145592686\u001b[0m\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 4.059182145592686\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mThe current objective is to raise Gigi Hadid's age to the 0.43 power. \n", + "\n", + "Action:\n", + "```\n", + "{\n", + " \"action\": \"Calculator\",\n", + " \"action_input\": \"26 ** 0.43\"\n", + "}\n", + "```\n", + "\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Entering new LLMMathChain chain...\u001b[0m\n", + "26 ** 0.43\u001b[32;1m\u001b[1;3m\n", + "```text\n", + "26 ** 0.43\n", + "```\n", + "...numexpr.evaluate(\"26 ** 0.43\")...\n", + "\u001b[0m\n", + "Answer: \u001b[33;1m\u001b[1;3m4.059182145592686\u001b[0m\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "\n", + "Observation: \u001b[33;1m\u001b[1;3mAnswer: 4.059182145592686\u001b[0m\n", + "Thought:\u001b[32;1m\u001b[1;3mThe answer to the current objective is 4.059182145592686.\n", + "\n", + "Action:\n", + "```\n", + "{\n", + " \"action\": \"Final Answer\",\n", + " \"action_input\": \"Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\"\n", + "}\n", + "```\n", + "\n", + "\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "*****\n", + "\n", + "Step: Raise her current age to the 0.43 power using a calculator or programming language.\n", + "\n", + "Response: Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mAction:\n", + "```\n", + "{\n", + " \"action\": \"Final Answer\",\n", + " \"action_input\": \"Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\"\n", + "}\n", + "```\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "*****\n", + "\n", + "Step: Output the result.\n", + "\n", + "Response: Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\n", + "\n", + "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", + "\u001b[32;1m\u001b[1;3mAction:\n", + "```\n", + "{\n", + " \"action\": \"Final Answer\",\n", + " \"action_input\": \"Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\"\n", + "}\n", + "```\n", + "\u001b[0m\n", + "\n", + "\u001b[1m> Finished chain.\u001b[0m\n", + "*****\n", + "\n", + "Step: Given the above steps taken, respond to the user's original question.\n", + "\n", + "\n", + "\n", + "Response: Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\n", + "\u001b[1m> Finished chain.\u001b[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Gigi Hadid's age raised to the 0.43 power is approximately 4.059 years.\"" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.run(\"Who is Leo DiCaprio's girlfriend? What is her current age raised to the 0.43 power?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa3ec998", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/agents/__init__.py b/langchain/agents/__init__.py index b70dea94..50596d05 100644 --- a/langchain/agents/__init__.py +++ b/langchain/agents/__init__.py @@ -26,6 +26,11 @@ from langchain.agents.initialize import initialize_agent from langchain.agents.load_tools import get_all_tool_names, load_tools from langchain.agents.loading import load_agent from langchain.agents.mrkl.base import MRKLChain, ZeroShotAgent +from langchain.agents.plan_and_execute.agent_executor import PlanAndExecute +from langchain.agents.plan_and_execute.executors.agent_executor import ( + load_agent_executor, +) +from langchain.agents.plan_and_execute.planners.chat_planner import load_chat_planner from langchain.agents.react.base import ReActChain, ReActTextWorldAgent from langchain.agents.self_ask_with_search.base import SelfAskWithSearchChain from langchain.agents.structured_chat.base import StructuredChatAgent @@ -63,4 +68,7 @@ __all__ = [ "load_agent", "load_tools", "tool", + "PlanAndExecute", + "load_chat_planner", + "load_agent_executor", ] diff --git a/langchain/agents/plan_and_execute/__init__.py b/langchain/agents/plan_and_execute/__init__.py new file mode 100644 index 00000000..3c706512 --- /dev/null +++ b/langchain/agents/plan_and_execute/__init__.py @@ -0,0 +1,7 @@ +from langchain.agents.plan_and_execute.agent_executor import PlanAndExecute +from langchain.agents.plan_and_execute.executors.agent_executor import ( + load_agent_executor, +) +from langchain.agents.plan_and_execute.planners.chat_planner import load_chat_planner + +__all__ = ["PlanAndExecute", "load_agent_executor", "load_chat_planner"] diff --git a/langchain/agents/plan_and_execute/agent_executor.py b/langchain/agents/plan_and_execute/agent_executor.py new file mode 100644 index 00000000..49be0ca7 --- /dev/null +++ b/langchain/agents/plan_and_execute/agent_executor.py @@ -0,0 +1,56 @@ +from typing import Any, Dict, List, Optional + +from pydantic import Field + +from langchain.agents.plan_and_execute.executors.base import BaseExecutor +from langchain.agents.plan_and_execute.planners.base import BasePlanner +from langchain.agents.plan_and_execute.schema import ( + BaseStepContainer, + ListStepContainer, +) +from langchain.callbacks.manager import CallbackManagerForChainRun +from langchain.chains.base import Chain + + +class PlanAndExecute(Chain): + planner: BasePlanner + executer: BaseExecutor + step_container: BaseStepContainer = Field(default_factory=ListStepContainer) + input_key: str = "input" + output_key: str = "output" + + @property + def input_keys(self) -> List[str]: + return [self.input_key] + + @property + def output_keys(self) -> List[str]: + return [self.output_key] + + def _call( + self, + inputs: Dict[str, Any], + run_manager: Optional[CallbackManagerForChainRun] = None, + ) -> Dict[str, Any]: + plan = self.planner.plan( + inputs, + callbacks=run_manager.get_child() if run_manager else None, + ) + if run_manager: + run_manager.on_text(str(plan), verbose=self.verbose) + for step in plan.steps: + _new_inputs = {"previous_steps": self.step_container, "current_step": step} + new_inputs = {**_new_inputs, **inputs} + response = self.executer.step( + new_inputs, + callbacks=run_manager.get_child() if run_manager else None, + ) + if run_manager: + run_manager.on_text( + f"*****\n\nStep: {step.value}", verbose=self.verbose + ) + run_manager.on_text( + f"\n\nResponse: {response.response}", verbose=self.verbose + ) + self.step_container.add_step(step, response) + return {self.output_key: self.step_container.get_final_response()} diff --git a/langchain/agents/plan_and_execute/executors/__init__.py b/langchain/agents/plan_and_execute/executors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/langchain/agents/plan_and_execute/executors/agent_executor.py b/langchain/agents/plan_and_execute/executors/agent_executor.py new file mode 100644 index 00000000..b852c5e8 --- /dev/null +++ b/langchain/agents/plan_and_execute/executors/agent_executor.py @@ -0,0 +1,28 @@ +from typing import List + +from langchain.agents.agent import AgentExecutor +from langchain.agents.plan_and_execute.executors.base import ChainExecutor +from langchain.agents.structured_chat.base import StructuredChatAgent +from langchain.base_language import BaseLanguageModel +from langchain.tools import BaseTool + +HUMAN_MESSAGE_TEMPLATE = """Previous steps: {previous_steps} + +Current objective: {current_step} + +{agent_scratchpad}""" + + +def load_agent_executor( + llm: BaseLanguageModel, tools: List[BaseTool], verbose: bool = False +) -> ChainExecutor: + agent = StructuredChatAgent.from_llm_and_tools( + llm, + tools, + human_message_template=HUMAN_MESSAGE_TEMPLATE, + input_variables=["previous_steps", "current_step", "agent_scratchpad"], + ) + agent_executor = AgentExecutor.from_agent_and_tools( + agent=agent, tools=tools, verbose=verbose + ) + return ChainExecutor(chain=agent_executor) diff --git a/langchain/agents/plan_and_execute/executors/base.py b/langchain/agents/plan_and_execute/executors/base.py new file mode 100644 index 00000000..b88b6918 --- /dev/null +++ b/langchain/agents/plan_and_execute/executors/base.py @@ -0,0 +1,40 @@ +from abc import abstractmethod +from typing import Any + +from pydantic import BaseModel + +from langchain.agents.plan_and_execute.schema import StepResponse +from langchain.callbacks.manager import Callbacks +from langchain.chains.base import Chain + + +class BaseExecutor(BaseModel): + @abstractmethod + def step( + self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any + ) -> StepResponse: + """Take step.""" + + @abstractmethod + async def astep( + self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any + ) -> StepResponse: + """Take step.""" + + +class ChainExecutor(BaseExecutor): + chain: Chain + + def step( + self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any + ) -> StepResponse: + """Take step.""" + response = self.chain.run(**inputs, callbacks=callbacks) + return StepResponse(response=response) + + async def astep( + self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any + ) -> StepResponse: + """Take step.""" + response = await self.chain.arun(**inputs, callbacks=callbacks) + return StepResponse(response=response) diff --git a/langchain/agents/plan_and_execute/planners/__init__.py b/langchain/agents/plan_and_execute/planners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/langchain/agents/plan_and_execute/planners/base.py b/langchain/agents/plan_and_execute/planners/base.py new file mode 100644 index 00000000..5bf6ac96 --- /dev/null +++ b/langchain/agents/plan_and_execute/planners/base.py @@ -0,0 +1,40 @@ +from abc import abstractmethod +from typing import Any, List, Optional + +from pydantic import BaseModel + +from langchain.agents.plan_and_execute.schema import Plan, PlanOutputParser +from langchain.callbacks.manager import Callbacks +from langchain.chains.llm import LLMChain + + +class BasePlanner(BaseModel): + @abstractmethod + def plan(self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any) -> Plan: + """Given input, decided what to do.""" + + @abstractmethod + async def aplan( + self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any + ) -> Plan: + """Given input, decided what to do.""" + + +class LLMPlanner(BasePlanner): + llm_chain: LLMChain + output_parser: PlanOutputParser + stop: Optional[List] = None + + def plan(self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any) -> Plan: + """Given input, decided what to do.""" + llm_response = self.llm_chain.run(**inputs, stop=self.stop, callbacks=callbacks) + return self.output_parser.parse(llm_response) + + async def aplan( + self, inputs: dict, callbacks: Callbacks = None, **kwargs: Any + ) -> Plan: + """Given input, decided what to do.""" + llm_response = await self.llm_chain.arun( + **inputs, stop=self.stop, callbacks=callbacks + ) + return self.output_parser.parse(llm_response) diff --git a/langchain/agents/plan_and_execute/planners/chat_planner.py b/langchain/agents/plan_and_execute/planners/chat_planner.py new file mode 100644 index 00000000..d6ea21d8 --- /dev/null +++ b/langchain/agents/plan_and_execute/planners/chat_planner.py @@ -0,0 +1,42 @@ +import re + +from langchain.agents.plan_and_execute.planners.base import LLMPlanner +from langchain.agents.plan_and_execute.schema import Plan, PlanOutputParser, Step +from langchain.base_language import BaseLanguageModel +from langchain.chains import LLMChain +from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate +from langchain.schema import SystemMessage + +SYSTEM_PROMPT = ( + "Let's first understand the problem and devise a plan to solve the problem." + " Please output the plan starting with the header 'Plan:' " + "and then followed by a numbered list of steps. " + "Please make the plan the minimum number of steps required " + "to accurately complete the task. If the task is a question, " + "the final step should almost always be 'Given the above steps taken, " + "please respond to the users original question'. " + "At the end of your plan, say ''" +) + + +class PlanningOutputParser(PlanOutputParser): + def parse(self, text: str) -> Plan: + steps = [Step(value=v) for v in re.split("\n\d+\. ", text)[1:]] + return Plan(steps=steps) + + +def load_chat_planner( + llm: BaseLanguageModel, system_prompt: str = SYSTEM_PROMPT +) -> LLMPlanner: + prompt_template = ChatPromptTemplate.from_messages( + [ + SystemMessage(content=system_prompt), + HumanMessagePromptTemplate.from_template("{input}"), + ] + ) + llm_chain = LLMChain(llm=llm, prompt=prompt_template) + return LLMPlanner( + llm_chain=llm_chain, + output_parser=PlanningOutputParser(), + stop=[""], + ) diff --git a/langchain/agents/plan_and_execute/schema.py b/langchain/agents/plan_and_execute/schema.py new file mode 100644 index 00000000..1cc04012 --- /dev/null +++ b/langchain/agents/plan_and_execute/schema.py @@ -0,0 +1,47 @@ +from abc import abstractmethod +from typing import List, Tuple + +from pydantic import BaseModel, Field + +from langchain.schema import BaseOutputParser + + +class Step(BaseModel): + value: str + + +class Plan(BaseModel): + steps: List[Step] + + +class StepResponse(BaseModel): + response: str + + +class BaseStepContainer(BaseModel): + @abstractmethod + def add_step(self, step: Step, step_response: StepResponse) -> None: + """Add step and step response to the container.""" + + @abstractmethod + def get_final_response(self) -> str: + """Return the final response based on steps taken.""" + + +class ListStepContainer(BaseModel): + steps: List[Tuple[Step, StepResponse]] = Field(default_factory=list) + + def add_step(self, step: Step, step_response: StepResponse) -> None: + self.steps.append((step, step_response)) + + def get_steps(self) -> List[Tuple[Step, StepResponse]]: + return self.steps + + def get_final_response(self) -> str: + return self.steps[-1][1].response + + +class PlanOutputParser(BaseOutputParser): + @abstractmethod + def parse(self, text: str) -> Plan: + """Parse into a plan.""" diff --git a/langchain/agents/structured_chat/base.py b/langchain/agents/structured_chat/base.py index bd6ec0f0..81bc26cc 100644 --- a/langchain/agents/structured_chat/base.py +++ b/langchain/agents/structured_chat/base.py @@ -20,6 +20,8 @@ from langchain.prompts.chat import ( from langchain.schema import AgentAction from langchain.tools import BaseTool +HUMAN_MESSAGE_TEMPLATE = "{input}\n\n{agent_scratchpad}" + class StructuredChatAgent(Agent): output_parser: AgentOutputParser = Field( @@ -71,6 +73,7 @@ class StructuredChatAgent(Agent): tools: Sequence[BaseTool], prefix: str = PREFIX, suffix: str = SUFFIX, + human_message_template: str = HUMAN_MESSAGE_TEMPLATE, format_instructions: str = FORMAT_INSTRUCTIONS, input_variables: Optional[List[str]] = None, ) -> BasePromptTemplate: @@ -84,7 +87,7 @@ class StructuredChatAgent(Agent): template = "\n\n".join([prefix, formatted_tools, format_instructions, suffix]) messages = [ SystemMessagePromptTemplate.from_template(template), - HumanMessagePromptTemplate.from_template("{input}\n\n{agent_scratchpad}"), + HumanMessagePromptTemplate.from_template(human_message_template), ] if input_variables is None: input_variables = ["input", "agent_scratchpad"] @@ -99,6 +102,7 @@ class StructuredChatAgent(Agent): output_parser: Optional[AgentOutputParser] = None, prefix: str = PREFIX, suffix: str = SUFFIX, + human_message_template: str = HUMAN_MESSAGE_TEMPLATE, format_instructions: str = FORMAT_INSTRUCTIONS, input_variables: Optional[List[str]] = None, **kwargs: Any, @@ -109,6 +113,7 @@ class StructuredChatAgent(Agent): tools, prefix=prefix, suffix=suffix, + human_message_template=human_message_template, format_instructions=format_instructions, input_variables=input_variables, ) diff --git a/tests/unit_tests/agents/test_public_api.py b/tests/unit_tests/agents/test_public_api.py index bc6fd4f3..89c6a288 100644 --- a/tests/unit_tests/agents/test_public_api.py +++ b/tests/unit_tests/agents/test_public_api.py @@ -32,9 +32,12 @@ _EXPECTED = [ "load_agent", "load_tools", "tool", + "PlanAndExecute", + "load_chat_planner", + "load_agent_executor", ] def test_public_api() -> None: """Test for regressions or changes in the agents public API.""" - assert agents_all == sorted(_EXPECTED) + assert sorted(agents_all) == sorted(_EXPECTED)