diff --git a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index 98c130a52..c41653c50 100644 --- a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -26,7 +26,10 @@ "id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c", "metadata": {}, "outputs": [], - "source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai"] + "source": [ + "# %%capture --no-stderr\n", + "# %pip install -U langgraph langchain langchain_openai" + ] }, { "cell_type": "code", @@ -34,7 +37,24 @@ "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith.\n", + "# This will help you visualize and debug the control flow\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\"" + ] }, { "cell_type": "markdown", @@ -55,7 +75,24 @@ "id": "828479af-cf9c-4888-a365-599643a96b55", "metadata": {}, "outputs": [], - "source": ["from typing import List\n\nimport openai\n\n\n# This is flexible, but you can define your agent here, or call your agent API here.\ndef my_chat_bot(messages: List[dict]) -> dict:\n system_message = {\n \"role\": \"system\",\n \"content\": \"You are a customer support agent for an airline.\",\n }\n messages = [system_message] + messages\n completion = openai.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\"\n )\n return completion.choices[0].message.model_dump()"] + "source": [ + "from typing import List\n", + "\n", + "import openai\n", + "\n", + "\n", + "# This is flexible, but you can define your agent here, or call your agent API here.\n", + "def my_chat_bot(messages: List[dict]) -> dict:\n", + " system_message = {\n", + " \"role\": \"system\",\n", + " \"content\": \"You are a customer support agent for an airline.\",\n", + " }\n", + " messages = [system_message] + messages\n", + " completion = openai.chat.completions.create(\n", + " messages=messages, model=\"gpt-3.5-turbo\"\n", + " )\n", + " return completion.choices[0].message.model_dump()" + ] }, { "cell_type": "code", @@ -77,7 +114,9 @@ "output_type": "execute_result" } ], - "source": ["my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"] + "source": [ + "my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])" + ] }, { "cell_type": "markdown", @@ -96,7 +135,33 @@ "id": "32c147df-7f90-4b0d-9a6b-671677020353", "metadata": {}, "outputs": [], - "source": ["from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nsystem_prompt_template = \"\"\"You are a customer of an airline company. \\\nYou are interacting with a user who is a customer support person. \\\n\n{instructions}\n\nWhen you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt_template),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\ninstructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\nYou want them to give you ALL the money back. \\\nThis trip happened 5 years ago.\"\"\"\n\nprompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n\nmodel = ChatOpenAI()\n\nsimulated_user = prompt | model"] + "source": [ + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "system_prompt_template = \"\"\"You are a customer of an airline company. \\\n", + "You are interacting with a user who is a customer support person. \\\n", + "\n", + "{instructions}\n", + "\n", + "When you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt_template),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + ")\n", + "instructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\n", + "You want them to give you ALL the money back. \\\n", + "This trip happened 5 years ago.\"\"\"\n", + "\n", + "prompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n", + "\n", + "model = ChatOpenAI()\n", + "\n", + "simulated_user = prompt | model" + ] }, { "cell_type": "code", @@ -115,7 +180,12 @@ "output_type": "execute_result" } ], - "source": ["from langchain_core.messages import HumanMessage\n\nmessages = [HumanMessage(content=\"Hi! How can I help you?\")]\nsimulated_user.invoke({\"messages\": messages})"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "messages = [HumanMessage(content=\"Hi! How can I help you?\")]\n", + "simulated_user.invoke({\"messages\": messages})" + ] }, { "cell_type": "markdown", @@ -153,7 +223,20 @@ "id": "69e2a3a3-40f3-4223-9136-113738440be9", "metadata": {}, "outputs": [], - "source": ["from langchain_community.adapters.openai import convert_message_to_dict\nfrom langchain_core.messages import AIMessage\n\n\ndef chat_bot_node(messages):\n # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n messages = [convert_message_to_dict(m) for m in messages]\n # Call the chat bot\n chat_bot_response = my_chat_bot(messages)\n # Respond with an AI Message\n return AIMessage(content=chat_bot_response[\"content\"])"] + "source": [ + "from langchain_community.adapters.openai import convert_message_to_dict\n", + "from langchain_core.messages import AIMessage\n", + "\n", + "\n", + "def chat_bot_node(state):\n", + " messages = state[\"messages\"]\n", + " # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n", + " messages = [convert_message_to_dict(m) for m in messages]\n", + " # Call the chat bot\n", + " chat_bot_response = my_chat_bot(messages)\n", + " # Respond with an AI Message\n", + " return {\"messages\":[AIMessage(content=chat_bot_response[\"content\"])]}" + ] }, { "cell_type": "markdown", @@ -169,7 +252,26 @@ "id": "7cad7527-ffa5-4c30-8585-b54a7a18bd98", "metadata": {}, "outputs": [], - "source": ["def _swap_roles(messages):\n new_messages = []\n for m in messages:\n if isinstance(m, AIMessage):\n new_messages.append(HumanMessage(content=m.content))\n else:\n new_messages.append(AIMessage(content=m.content))\n return new_messages\n\n\ndef simulated_user_node(messages):\n # Swap roles of messages\n new_messages = _swap_roles(messages)\n # Call the simulated user\n response = simulated_user.invoke({\"messages\": new_messages})\n # This response is an AI message - we need to flip this to be a human message\n return HumanMessage(content=response.content)"] + "source": [ + "def _swap_roles(messages):\n", + " new_messages = []\n", + " for m in messages:\n", + " if isinstance(m, AIMessage):\n", + " new_messages.append(HumanMessage(content=m.content))\n", + " else:\n", + " new_messages.append(AIMessage(content=m.content))\n", + " return new_messages\n", + "\n", + "\n", + "def simulated_user_node(state):\n", + " messages = state[\"messages\"]\n", + " # Swap roles of messages\n", + " new_messages = _swap_roles(messages)\n", + " # Call the simulated user\n", + " response = simulated_user.invoke({\"messages\": new_messages})\n", + " # This response is an AI message - we need to flip this to be a human message\n", + " return {\"messages\":[HumanMessage(content=response.content)]}" + ] }, { "cell_type": "markdown", @@ -192,7 +294,16 @@ "id": "28004fbf-a2f3-46b7-bde7-46c7adaf97fb", "metadata": {}, "outputs": [], - "source": ["def should_continue(messages):\n if len(messages) > 6:\n return \"end\"\n elif messages[-1].content == \"FINISHED\":\n return \"end\"\n else:\n return \"continue\""] + "source": [ + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " if len(messages) > 6:\n", + " return \"end\"\n", + " elif messages[-1].content == \"FINISHED\":\n", + " return \"end\"\n", + " else:\n", + " return \"continue\"" + ] }, { "cell_type": "markdown", @@ -210,7 +321,36 @@ "id": "0b597e4b-4cbb-4bbc-82e5-f7e31275964c", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, MessageGraph, START\n\ngraph_builder = MessageGraph()\ngraph_builder.add_node(\"user\", simulated_user_node)\ngraph_builder.add_node(\"chat_bot\", chat_bot_node)\n# Every response from your chat bot will automatically go to the\n# simulated user\ngraph_builder.add_edge(\"chat_bot\", \"user\")\ngraph_builder.add_conditional_edges(\n \"user\",\n should_continue,\n # If the finish criteria are met, we will stop the simulation,\n # otherwise, the virtual user's message will be sent to your chat bot\n {\n \"end\": END,\n \"continue\": \"chat_bot\",\n },\n)\n# The input will first go to your chat bot\ngraph_builder.add_edge(START, \"chat_bot\")\nsimulation = graph_builder.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.graph.message import add_messages\n", + "from typing import Annotated\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + "\n", + "graph_builder = StateGraph(State)\n", + "graph_builder.add_node(\"user\", simulated_user_node)\n", + "graph_builder.add_node(\"chat_bot\", chat_bot_node)\n", + "# Every response from your chat bot will automatically go to the\n", + "# simulated user\n", + "graph_builder.add_edge(\"chat_bot\", \"user\")\n", + "graph_builder.add_conditional_edges(\n", + " \"user\",\n", + " should_continue,\n", + " # If the finish criteria are met, we will stop the simulation,\n", + " # otherwise, the virtual user's message will be sent to your chat bot\n", + " {\n", + " \"end\": END,\n", + " \"continue\": \"chat_bot\",\n", + " },\n", + ")\n", + "# The input will first go to your chat bot\n", + "graph_builder.add_edge(START, \"chat_bot\")\n", + "simulation = graph_builder.compile()" + ] }, { "cell_type": "markdown", @@ -251,7 +391,13 @@ ] } ], - "source": ["for chunk in simulation.stream([]):\n # Print out all events aside from the final end chunk\n if END not in chunk:\n print(chunk)\n print(\"----\")"] + "source": [ + "for chunk in simulation.stream({}):\n", + " # Print out all events aside from the final end chunk\n", + " if END not in chunk:\n", + " print(chunk)\n", + " print(\"----\")" + ] }, { "cell_type": "code", @@ -259,7 +405,7 @@ "id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0", "metadata": {}, "outputs": [], - "source": [""] + "source": [] } ], "metadata": { diff --git a/examples/chatbots/information-gather-prompting.ipynb b/examples/chatbots/information-gather-prompting.ipynb index 079478dc7..bc1248cda 100644 --- a/examples/chatbots/information-gather-prompting.ipynb +++ b/examples/chatbots/information-gather-prompting.ipynb @@ -177,10 +177,16 @@ "outputs": [], "source": [ "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import START, MessageGraph\n", + "from langgraph.graph import StateGraph, START\n", + "from langgraph.graph.message import add_messages\n", + "from typing import Annotated\n", + "from typing_extensions import TypedDict\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", "\n", "memory = MemorySaver()\n", - "workflow = MessageGraph()\n", + "workflow = StateGraph(State)\n", "workflow.add_node(\"info\", chain)\n", "workflow.add_node(\"prompt\", prompt_gen_chain)\n", "\n", diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index 1b9387e15..bfe5736d8 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -32,7 +32,9 @@ "id": "16bd5497-35ad-44f2-94d9-19ff39a5ffed", "metadata": {}, "outputs": [], - "source": ["# %pip install -U --quiet langchain_openai langsmith langgraph langchain numexpr"] + "source": [ + "# %pip install -U --quiet langchain_openai langsmith langgraph langchain numexpr" + ] }, { "cell_type": "code", @@ -40,7 +42,22 @@ "id": "abbd6948-e9a3-47ca-89c7-7ac2fc5eca8b", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _get_pass(var: str):\n if var not in os.environ:\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n# Optional: Debug + trace calls using LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"True\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"LLMCompiler\"\n_get_pass(\"LANGCHAIN_API_KEY\")\n_get_pass(\"OPENAI_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _get_pass(var: str):\n", + " if var not in os.environ:\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "# Optional: Debug + trace calls using LangSmith\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"True\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"LLMCompiler\"\n", + "_get_pass(\"LANGCHAIN_API_KEY\")\n", + "_get_pass(\"OPENAI_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -60,7 +77,23 @@ "id": "e7476bb2-1a51-42f6-b7ae-82a0300bbf84", "metadata": {}, "outputs": [], - "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai import ChatOpenAI\n\n# Imported from the https://github.com/langchain-ai/langgraph/tree/main/examples/plan-and-execute repo\nfrom math_tools import get_math_tool\n\n_get_pass(\"TAVILY_API_KEY\")\n\ncalculate = get_math_tool(ChatOpenAI(model=\"gpt-4-turbo-preview\"))\nsearch = TavilySearchResults(\n max_results=1,\n description='tavily_search_results_json(query=\"the search query\") - a search engine.',\n)\n\ntools = [search, calculate]"] + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "# Imported from the https://github.com/langchain-ai/langgraph/tree/main/examples/plan-and-execute repo\n", + "from math_tools import get_math_tool\n", + "\n", + "_get_pass(\"TAVILY_API_KEY\")\n", + "\n", + "calculate = get_math_tool(ChatOpenAI(model=\"gpt-4-turbo-preview\"))\n", + "search = TavilySearchResults(\n", + " max_results=1,\n", + " description='tavily_search_results_json(query=\"the search query\") - a search engine.',\n", + ")\n", + "\n", + "tools = [search, calculate]" + ] }, { "cell_type": "code", @@ -79,7 +112,14 @@ "output_type": "execute_result" } ], - "source": ["calculate.invoke(\n {\n \"problem\": \"What's the temp of sf + 5?\",\n \"context\": [\"Thet empreature of sf is 32 degrees\"],\n }\n)"] + "source": [ + "calculate.invoke(\n", + " {\n", + " \"problem\": \"What's the temp of sf + 5?\",\n", + " \"context\": [\"Thet empreature of sf is 32 degrees\"],\n", + " }\n", + ")" + ] }, { "cell_type": "markdown", @@ -148,7 +188,26 @@ ] } ], - "source": ["from typing import Sequence\n\nfrom langchain import hub\nfrom langchain_core.language_models import BaseChatModel\nfrom langchain_core.messages import (\n BaseMessage,\n FunctionMessage,\n HumanMessage,\n SystemMessage,\n)\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import RunnableBranch\nfrom langchain_core.tools import BaseTool\nfrom langchain_openai import ChatOpenAI\nfrom output_parser import LLMCompilerPlanParser, Task\n\nprompt = hub.pull(\"wfh/llm-compiler\")\nprint(prompt.pretty_print())"] + "source": [ + "from typing import Sequence\n", + "\n", + "from langchain import hub\n", + "from langchain_core.language_models import BaseChatModel\n", + "from langchain_core.messages import (\n", + " BaseMessage,\n", + " FunctionMessage,\n", + " HumanMessage,\n", + " SystemMessage,\n", + ")\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.runnables import RunnableBranch\n", + "from langchain_core.tools import BaseTool\n", + "from langchain_openai import ChatOpenAI\n", + "from output_parser import LLMCompilerPlanParser, Task\n", + "\n", + "prompt = hub.pull(\"wfh/llm-compiler\")\n", + "print(prompt.pretty_print())" + ] }, { "cell_type": "code", @@ -156,7 +215,58 @@ "id": "45689d40-d8df-4316-a121-6ea9c87d2efe", "metadata": {}, "outputs": [], - "source": ["def create_planner(\n llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate\n):\n tool_descriptions = \"\\n\".join(\n f\"{i+1}. {tool.description}\\n\"\n for i, tool in enumerate(\n tools\n ) # +1 to offset the 0 starting index, we want it count normally from 1.\n )\n planner_prompt = base_prompt.partial(\n replan=\"\",\n num_tools=len(tools)\n + 1, # Add one because we're adding the join() tool at the end.\n tool_descriptions=tool_descriptions,\n )\n replanner_prompt = base_prompt.partial(\n replan=' - You are given \"Previous Plan\" which is the plan that the previous agent created along with the execution results '\n \"(given as Observation) of each plan and a general thought (given as Thought) about the executed results.\"\n 'You MUST use these information to create the next plan under \"Current Plan\".\\n'\n ' - When starting the Current Plan, you should start with \"Thought\" that outlines the strategy for the next plan.\\n'\n \" - In the Current Plan, you should NEVER repeat the actions that are already executed in the Previous Plan.\\n\"\n \" - You must continue the task index from the end of the previous one. Do not repeat task indices.\",\n num_tools=len(tools) + 1,\n tool_descriptions=tool_descriptions,\n )\n\n def should_replan(state: list):\n # Context is passed as a system message\n return isinstance(state[-1], SystemMessage)\n\n def wrap_messages(state: list):\n return {\"messages\": state}\n\n def wrap_and_get_last_index(state: list):\n next_task = 0\n for message in state[::-1]:\n if isinstance(message, FunctionMessage):\n next_task = message.additional_kwargs[\"idx\"] + 1\n break\n state[-1].content = state[-1].content + f\" - Begin counting at : {next_task}\"\n return {\"messages\": state}\n\n return (\n RunnableBranch(\n (should_replan, wrap_and_get_last_index | replanner_prompt),\n wrap_messages | planner_prompt,\n )\n | llm\n | LLMCompilerPlanParser(tools=tools)\n )"] + "source": [ + "def create_planner(\n", + " llm: BaseChatModel, tools: Sequence[BaseTool], base_prompt: ChatPromptTemplate\n", + "):\n", + " tool_descriptions = \"\\n\".join(\n", + " f\"{i+1}. {tool.description}\\n\"\n", + " for i, tool in enumerate(\n", + " tools\n", + " ) # +1 to offset the 0 starting index, we want it count normally from 1.\n", + " )\n", + " planner_prompt = base_prompt.partial(\n", + " replan=\"\",\n", + " num_tools=len(tools)\n", + " + 1, # Add one because we're adding the join() tool at the end.\n", + " tool_descriptions=tool_descriptions,\n", + " )\n", + " replanner_prompt = base_prompt.partial(\n", + " replan=' - You are given \"Previous Plan\" which is the plan that the previous agent created along with the execution results '\n", + " \"(given as Observation) of each plan and a general thought (given as Thought) about the executed results.\"\n", + " 'You MUST use these information to create the next plan under \"Current Plan\".\\n'\n", + " ' - When starting the Current Plan, you should start with \"Thought\" that outlines the strategy for the next plan.\\n'\n", + " \" - In the Current Plan, you should NEVER repeat the actions that are already executed in the Previous Plan.\\n\"\n", + " \" - You must continue the task index from the end of the previous one. Do not repeat task indices.\",\n", + " num_tools=len(tools) + 1,\n", + " tool_descriptions=tool_descriptions,\n", + " )\n", + "\n", + " def should_replan(state: list):\n", + " # Context is passed as a system message\n", + " return isinstance(state[-1], SystemMessage)\n", + "\n", + " def wrap_messages(state: list):\n", + " return {\"messages\": state}\n", + "\n", + " def wrap_and_get_last_index(state: list):\n", + " next_task = 0\n", + " for message in state[::-1]:\n", + " if isinstance(message, FunctionMessage):\n", + " next_task = message.additional_kwargs[\"idx\"] + 1\n", + " break\n", + " state[-1].content = state[-1].content + f\" - Begin counting at : {next_task}\"\n", + " return {\"messages\": state}\n", + "\n", + " return (\n", + " RunnableBranch(\n", + " (should_replan, wrap_and_get_last_index | replanner_prompt),\n", + " wrap_messages | planner_prompt,\n", + " )\n", + " | llm\n", + " | LLMCompilerPlanParser(tools=tools)\n", + " )" + ] }, { "cell_type": "code", @@ -164,7 +274,11 @@ "id": "bbdcb57b-5362-4b9e-88db-fb3fae443fb0", "metadata": {}, "outputs": [], - "source": ["llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n# This is the primary \"agent\" in our application\nplanner = create_planner(llm, tools, prompt)"] + "source": [ + "llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", + "# This is the primary \"agent\" in our application\n", + "planner = create_planner(llm, tools, prompt)" + ] }, { "cell_type": "code", @@ -185,7 +299,13 @@ ] } ], - "source": ["example_question = \"What's the temperature in SF raised to the 3rd power?\"\n\nfor task in planner.stream([HumanMessage(content=example_question)]):\n print(task[\"tool\"], task[\"args\"])\n print(\"---\")"] + "source": [ + "example_question = \"What's the temperature in SF raised to the 3rd power?\"\n", + "\n", + "for task in planner.stream([HumanMessage(content=example_question)]):\n", + " print(task[\"tool\"], task[\"args\"])\n", + " print(\"---\")" + ] }, { "cell_type": "markdown", @@ -217,7 +337,168 @@ "jp-MarkdownHeadingCollapsed": true }, "outputs": [], - "source": ["import re\nimport time\nfrom concurrent.futures import ThreadPoolExecutor, wait\nfrom typing import Any, Dict, Iterable, List, Union\n\nfrom langchain_core.runnables import (\n chain as as_runnable,\n)\nfrom typing_extensions import TypedDict\n\n\ndef _get_observations(messages: List[BaseMessage]) -> Dict[int, Any]:\n # Get all previous tool responses\n results = {}\n for message in messages[::-1]:\n if isinstance(message, FunctionMessage):\n results[int(message.additional_kwargs[\"idx\"])] = message.content\n return results\n\n\nclass SchedulerInput(TypedDict):\n messages: List[BaseMessage]\n tasks: Iterable[Task]\n\n\ndef _execute_task(task, observations, config):\n tool_to_use = task[\"tool\"]\n if isinstance(tool_to_use, str):\n return tool_to_use\n args = task[\"args\"]\n try:\n if isinstance(args, str):\n resolved_args = _resolve_arg(args, observations)\n elif isinstance(args, dict):\n resolved_args = {\n key: _resolve_arg(val, observations) for key, val in args.items()\n }\n else:\n # This will likely fail\n resolved_args = args\n except Exception as e:\n return (\n f\"ERROR(Failed to call {tool_to_use.name} with args {args}.)\"\n f\" Args could not be resolved. Error: {repr(e)}\"\n )\n try:\n return tool_to_use.invoke(resolved_args, config)\n except Exception as e:\n return (\n f\"ERROR(Failed to call {tool_to_use.name} with args {args}.\"\n + f\" Args resolved to {resolved_args}. Error: {repr(e)})\"\n )\n\n\ndef _resolve_arg(arg: Union[str, Any], observations: Dict[int, Any]):\n # $1 or ${1} -> 1\n ID_PATTERN = r\"\\$\\{?(\\d+)\\}?\"\n\n def replace_match(match):\n # If the string is ${123}, match.group(0) is ${123}, and match.group(1) is 123.\n\n # Return the match group, in this case the index, from the string. This is the index\n # number we get back.\n idx = int(match.group(1))\n return str(observations.get(idx, match.group(0)))\n\n # For dependencies on other tasks\n if isinstance(arg, str):\n return re.sub(ID_PATTERN, replace_match, arg)\n elif isinstance(arg, list):\n return [_resolve_arg(a, observations) for a in arg]\n else:\n return str(arg)\n\n\n@as_runnable\ndef schedule_task(task_inputs, config):\n task: Task = task_inputs[\"task\"]\n observations: Dict[int, Any] = task_inputs[\"observations\"]\n try:\n observation = _execute_task(task, observations, config)\n except Exception:\n import traceback\n\n observation = traceback.format_exception() # repr(e) +\n observations[task[\"idx\"]] = observation\n\n\ndef schedule_pending_task(\n task: Task, observations: Dict[int, Any], retry_after: float = 0.2\n):\n while True:\n deps = task[\"dependencies\"]\n if deps and (any([dep not in observations for dep in deps])):\n # Dependencies not yet satisfied\n time.sleep(retry_after)\n continue\n schedule_task.invoke({\"task\": task, \"observations\": observations})\n break\n\n\n@as_runnable\ndef schedule_tasks(scheduler_input: SchedulerInput) -> List[FunctionMessage]:\n \"\"\"Group the tasks into a DAG schedule.\"\"\"\n # For streaming, we are making a few simplifying assumption:\n # 1. The LLM does not create cyclic dependencies\n # 2. That the LLM will not generate tasks with future deps\n # If this ceases to be a good assumption, you can either\n # adjust to do a proper topological sort (not-stream)\n # or use a more complicated data structure\n tasks = scheduler_input[\"tasks\"]\n args_for_tasks = {}\n messages = scheduler_input[\"messages\"]\n # If we are re-planning, we may have calls that depend on previous\n # plans. Start with those.\n observations = _get_observations(messages)\n task_names = {}\n originals = set(observations)\n # ^^ We assume each task inserts a different key above to\n # avoid race conditions...\n futures = []\n retry_after = 0.25 # Retry every quarter second\n with ThreadPoolExecutor() as executor:\n for task in tasks:\n deps = task[\"dependencies\"]\n task_names[task[\"idx\"]] = (\n task[\"tool\"] if isinstance(task[\"tool\"], str) else task[\"tool\"].name\n )\n args_for_tasks[task[\"idx\"]] = task[\"args\"]\n if (\n # Depends on other tasks\n deps\n and (any([dep not in observations for dep in deps]))\n ):\n futures.append(\n executor.submit(\n schedule_pending_task, task, observations, retry_after\n )\n )\n else:\n # No deps or all deps satisfied\n # can schedule now\n schedule_task.invoke(dict(task=task, observations=observations))\n # futures.append(executor.submit(schedule_task.invoke dict(task=task, observations=observations)))\n\n # All tasks have been submitted or enqueued\n # Wait for them to complete\n wait(futures)\n # Convert observations to new tool messages to add to the state\n new_observations = {\n k: (task_names[k], args_for_tasks[k], observations[k])\n for k in sorted(observations.keys() - originals)\n }\n tool_messages = [\n FunctionMessage(\n name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}\n )\n for k, (name, task_args, obs) in new_observations.items()\n ]\n return tool_messages"] + "source": [ + "import re\n", + "import time\n", + "from concurrent.futures import ThreadPoolExecutor, wait\n", + "from typing import Any, Dict, Iterable, List, Union\n", + "\n", + "from langchain_core.runnables import (\n", + " chain as as_runnable,\n", + ")\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "def _get_observations(messages: List[BaseMessage]) -> Dict[int, Any]:\n", + " # Get all previous tool responses\n", + " results = {}\n", + " for message in messages[::-1]:\n", + " if isinstance(message, FunctionMessage):\n", + " results[int(message.additional_kwargs[\"idx\"])] = message.content\n", + " return results\n", + "\n", + "\n", + "class SchedulerInput(TypedDict):\n", + " messages: List[BaseMessage]\n", + " tasks: Iterable[Task]\n", + "\n", + "\n", + "def _execute_task(task, observations, config):\n", + " tool_to_use = task[\"tool\"]\n", + " if isinstance(tool_to_use, str):\n", + " return tool_to_use\n", + " args = task[\"args\"]\n", + " try:\n", + " if isinstance(args, str):\n", + " resolved_args = _resolve_arg(args, observations)\n", + " elif isinstance(args, dict):\n", + " resolved_args = {\n", + " key: _resolve_arg(val, observations) for key, val in args.items()\n", + " }\n", + " else:\n", + " # This will likely fail\n", + " resolved_args = args\n", + " except Exception as e:\n", + " return (\n", + " f\"ERROR(Failed to call {tool_to_use.name} with args {args}.)\"\n", + " f\" Args could not be resolved. Error: {repr(e)}\"\n", + " )\n", + " try:\n", + " return tool_to_use.invoke(resolved_args, config)\n", + " except Exception as e:\n", + " return (\n", + " f\"ERROR(Failed to call {tool_to_use.name} with args {args}.\"\n", + " + f\" Args resolved to {resolved_args}. Error: {repr(e)})\"\n", + " )\n", + "\n", + "\n", + "def _resolve_arg(arg: Union[str, Any], observations: Dict[int, Any]):\n", + " # $1 or ${1} -> 1\n", + " ID_PATTERN = r\"\\$\\{?(\\d+)\\}?\"\n", + "\n", + " def replace_match(match):\n", + " # If the string is ${123}, match.group(0) is ${123}, and match.group(1) is 123.\n", + "\n", + " # Return the match group, in this case the index, from the string. This is the index\n", + " # number we get back.\n", + " idx = int(match.group(1))\n", + " return str(observations.get(idx, match.group(0)))\n", + "\n", + " # For dependencies on other tasks\n", + " if isinstance(arg, str):\n", + " return re.sub(ID_PATTERN, replace_match, arg)\n", + " elif isinstance(arg, list):\n", + " return [_resolve_arg(a, observations) for a in arg]\n", + " else:\n", + " return str(arg)\n", + "\n", + "\n", + "@as_runnable\n", + "def schedule_task(task_inputs, config):\n", + " task: Task = task_inputs[\"task\"]\n", + " observations: Dict[int, Any] = task_inputs[\"observations\"]\n", + " try:\n", + " observation = _execute_task(task, observations, config)\n", + " except Exception:\n", + " import traceback\n", + "\n", + " observation = traceback.format_exception() # repr(e) +\n", + " observations[task[\"idx\"]] = observation\n", + "\n", + "\n", + "def schedule_pending_task(\n", + " task: Task, observations: Dict[int, Any], retry_after: float = 0.2\n", + "):\n", + " while True:\n", + " deps = task[\"dependencies\"]\n", + " if deps and (any([dep not in observations for dep in deps])):\n", + " # Dependencies not yet satisfied\n", + " time.sleep(retry_after)\n", + " continue\n", + " schedule_task.invoke({\"task\": task, \"observations\": observations})\n", + " break\n", + "\n", + "\n", + "@as_runnable\n", + "def schedule_tasks(scheduler_input: SchedulerInput) -> List[FunctionMessage]:\n", + " \"\"\"Group the tasks into a DAG schedule.\"\"\"\n", + " # For streaming, we are making a few simplifying assumption:\n", + " # 1. The LLM does not create cyclic dependencies\n", + " # 2. That the LLM will not generate tasks with future deps\n", + " # If this ceases to be a good assumption, you can either\n", + " # adjust to do a proper topological sort (not-stream)\n", + " # or use a more complicated data structure\n", + " tasks = scheduler_input[\"tasks\"]\n", + " args_for_tasks = {}\n", + " messages = scheduler_input[\"messages\"]\n", + " # If we are re-planning, we may have calls that depend on previous\n", + " # plans. Start with those.\n", + " observations = _get_observations(messages)\n", + " task_names = {}\n", + " originals = set(observations)\n", + " # ^^ We assume each task inserts a different key above to\n", + " # avoid race conditions...\n", + " futures = []\n", + " retry_after = 0.25 # Retry every quarter second\n", + " with ThreadPoolExecutor() as executor:\n", + " for task in tasks:\n", + " deps = task[\"dependencies\"]\n", + " task_names[task[\"idx\"]] = (\n", + " task[\"tool\"] if isinstance(task[\"tool\"], str) else task[\"tool\"].name\n", + " )\n", + " args_for_tasks[task[\"idx\"]] = task[\"args\"]\n", + " if (\n", + " # Depends on other tasks\n", + " deps\n", + " and (any([dep not in observations for dep in deps]))\n", + " ):\n", + " futures.append(\n", + " executor.submit(\n", + " schedule_pending_task, task, observations, retry_after\n", + " )\n", + " )\n", + " else:\n", + " # No deps or all deps satisfied\n", + " # can schedule now\n", + " schedule_task.invoke(dict(task=task, observations=observations))\n", + " # futures.append(executor.submit(schedule_task.invoke dict(task=task, observations=observations)))\n", + "\n", + " # All tasks have been submitted or enqueued\n", + " # Wait for them to complete\n", + " wait(futures)\n", + " # Convert observations to new tool messages to add to the state\n", + " new_observations = {\n", + " k: (task_names[k], args_for_tasks[k], observations[k])\n", + " for k in sorted(observations.keys() - originals)\n", + " }\n", + " tool_messages = [\n", + " FunctionMessage(\n", + " name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}\n", + " )\n", + " for k, (name, task_args, obs) in new_observations.items()\n", + " ]\n", + " return tool_messages" + ] }, { "cell_type": "code", @@ -225,7 +506,28 @@ "id": "052f6b16-103a-40e9-94dd-8fcc37e77ba4", "metadata": {}, "outputs": [], - "source": ["import itertools\n\n\n@as_runnable\ndef plan_and_schedule(messages: List[BaseMessage], config):\n tasks = planner.stream(messages, config)\n # Begin executing the planner immediately\n try:\n tasks = itertools.chain([next(tasks)], tasks)\n except StopIteration:\n # Handle the case where tasks is empty.\n tasks = iter([])\n scheduled_tasks = schedule_tasks.invoke(\n {\n \"messages\": messages,\n \"tasks\": tasks,\n },\n config,\n )\n return scheduled_tasks"] + "source": [ + "import itertools\n", + "\n", + "\n", + "@as_runnable\n", + "def plan_and_schedule(state):\n", + " messages = state[\"messages\"]\n", + " tasks = planner.stream(messages)\n", + " # Begin executing the planner immediately\n", + " try:\n", + " tasks = itertools.chain([next(tasks)], tasks)\n", + " except StopIteration:\n", + " # Handle the case where tasks is empty.\n", + " tasks = iter([])\n", + " scheduled_tasks = schedule_tasks.invoke(\n", + " {\n", + " \"messages\": messages,\n", + " \"tasks\": tasks,\n", + " }\n", + " )\n", + " return {\"messages\":[scheduled_tasks]}" + ] }, { "cell_type": "markdown", @@ -243,7 +545,9 @@ "id": "55142257-2674-4a47-988e-0d2810917329", "metadata": {}, "outputs": [], - "source": ["tool_messages = plan_and_schedule.invoke([HumanMessage(content=example_question)])"] + "source": [ + "tool_messages = plan_and_schedule.invoke([HumanMessage(content=example_question)])" + ] }, { "cell_type": "code", @@ -264,7 +568,9 @@ "output_type": "execute_result" } ], - "source": ["tool_messages"] + "source": [ + "tool_messages" + ] }, { "cell_type": "markdown", @@ -287,7 +593,40 @@ "id": "942dab42-ad42-4ba2-90d5-49edbe4fae68", "metadata": {}, "outputs": [], - "source": ["from langchain.chains.openai_functions import create_structured_output_runnable\nfrom langchain_core.messages import AIMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass FinalResponse(BaseModel):\n \"\"\"The final response/answer.\"\"\"\n\n response: str\n\n\nclass Replan(BaseModel):\n feedback: str = Field(\n description=\"Analysis of the previous attempts and recommendations on what needs to be fixed.\"\n )\n\n\nclass JoinOutputs(BaseModel):\n \"\"\"Decide whether to replan or whether you can return the final response.\"\"\"\n\n thought: str = Field(\n description=\"The chain of thought reasoning for the selected action\"\n )\n action: Union[FinalResponse, Replan]\n\n\njoiner_prompt = hub.pull(\"wfh/llm-compiler-joiner\").partial(\n examples=\"\"\n) # You can optionally add examples\nllm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nrunnable = create_structured_output_runnable(JoinOutputs, llm, joiner_prompt)"] + "source": [ + "from langchain.chains.openai_functions import create_structured_output_runnable\n", + "from langchain_core.messages import AIMessage\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "\n", + "class FinalResponse(BaseModel):\n", + " \"\"\"The final response/answer.\"\"\"\n", + "\n", + " response: str\n", + "\n", + "\n", + "class Replan(BaseModel):\n", + " feedback: str = Field(\n", + " description=\"Analysis of the previous attempts and recommendations on what needs to be fixed.\"\n", + " )\n", + "\n", + "\n", + "class JoinOutputs(BaseModel):\n", + " \"\"\"Decide whether to replan or whether you can return the final response.\"\"\"\n", + "\n", + " thought: str = Field(\n", + " description=\"The chain of thought reasoning for the selected action\"\n", + " )\n", + " action: Union[FinalResponse, Replan]\n", + "\n", + "\n", + "joiner_prompt = hub.pull(\"wfh/llm-compiler-joiner\").partial(\n", + " examples=\"\"\n", + ") # You can optionally add examples\n", + "llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", + "\n", + "runnable = create_structured_output_runnable(JoinOutputs, llm, joiner_prompt)" + ] }, { "cell_type": "markdown", @@ -304,7 +643,31 @@ "id": "951a33cf-2a05-4a33-899a-0ab1d97122fa", "metadata": {}, "outputs": [], - "source": ["def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n if isinstance(decision.action, Replan):\n return response + [\n SystemMessage(\n content=f\"Context from last attempt: {decision.action.feedback}\"\n )\n ]\n else:\n return response + [AIMessage(content=decision.action.response)]\n\n\ndef select_recent_messages(messages: list) -> dict:\n selected = []\n for msg in messages[::-1]:\n selected.append(msg)\n if isinstance(msg, HumanMessage):\n break\n return {\"messages\": selected[::-1]}\n\n\njoiner = select_recent_messages | runnable | _parse_joiner_output"] + "source": [ + "def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n", + " response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n", + " if isinstance(decision.action, Replan):\n", + " return response + [\n", + " SystemMessage(\n", + " content=f\"Context from last attempt: {decision.action.feedback}\"\n", + " )\n", + " ]\n", + " else:\n", + " return {\"messages\":response + [AIMessage(content=decision.action.response)]}\n", + "\n", + "\n", + "def select_recent_messages(state) -> dict:\n", + " messages = state[\"messages\"]\n", + " selected = []\n", + " for msg in messages[::-1]:\n", + " selected.append(msg)\n", + " if isinstance(msg, HumanMessage):\n", + " break\n", + " return {\"messages\": selected[::-1]}\n", + "\n", + "\n", + "joiner = select_recent_messages | runnable | _parse_joiner_output" + ] }, { "cell_type": "code", @@ -312,7 +675,9 @@ "id": "1e49d4b1-8266-4520-a566-1448b1c31c8f", "metadata": {}, "outputs": [], - "source": ["input_messages = [HumanMessage(content=example_question)] + tool_messages"] + "source": [ + "input_messages = [HumanMessage(content=example_question)] + tool_messages" + ] }, { "cell_type": "code", @@ -332,7 +697,9 @@ "output_type": "execute_result" } ], - "source": ["joiner.invoke(input_messages)"] + "source": [ + "joiner.invoke(input_messages)" + ] }, { "cell_type": "markdown", @@ -354,7 +721,44 @@ "id": "768b5f11-e3d2-47be-8143-a7dcd8765243", "metadata": {}, "outputs": [], - "source": ["from typing import Dict\n\nfrom langgraph.graph import END, MessageGraph, START\n\ngraph_builder = MessageGraph()\n\n# 1. Define vertices\n# We defined plan_and_schedule above already\n# Assign each node to a state variable to update\ngraph_builder.add_node(\"plan_and_schedule\", plan_and_schedule)\ngraph_builder.add_node(\"join\", joiner)\n\n\n## Define edges\ngraph_builder.add_edge(\"plan_and_schedule\", \"join\")\n\n### This condition determines looping logic\n\n\ndef should_continue(state: List[BaseMessage]):\n if isinstance(state[-1], AIMessage):\n return END\n return \"plan_and_schedule\"\n\n\ngraph_builder.add_conditional_edges(\n start_key=\"join\",\n # Next, we pass in the function that will determine which node is called next.\n condition=should_continue,\n)\ngraph_builder.add_edge(START, \"plan_and_schedule\")\nchain = graph_builder.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.graph.message import add_messages\n", + "from typing import Annotated\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + "\n", + "graph_builder = StateGraph(State)\n", + "\n", + "# 1. Define vertices\n", + "# We defined plan_and_schedule above already\n", + "# Assign each node to a state variable to update\n", + "graph_builder.add_node(\"plan_and_schedule\", plan_and_schedule)\n", + "graph_builder.add_node(\"join\", joiner)\n", + "\n", + "\n", + "## Define edges\n", + "graph_builder.add_edge(\"plan_and_schedule\", \"join\")\n", + "\n", + "### This condition determines looping logic\n", + "\n", + "\n", + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " if isinstance(messages[-1], AIMessage):\n", + " return END\n", + " return \"plan_and_schedule\"\n", + "\n", + "\n", + "graph_builder.add_conditional_edges(\n", + " start_key=\"join\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " condition=should_continue,\n", + ")\n", + "graph_builder.add_edge(START, \"plan_and_schedule\")\n", + "chain = graph_builder.compile()" + ] }, { "cell_type": "markdown", @@ -389,7 +793,11 @@ ] } ], - "source": ["for step in chain.stream([HumanMessage(content=\"What's the GDP of New York?\")]):\n print(step)\n print(\"---\")"] + "source": [ + "for step in chain.stream({\"messages\":[HumanMessage(content=\"What's the GDP of New York?\")]}):\n", + " print(step)\n", + " print(\"---\")" + ] }, { "cell_type": "code", @@ -405,7 +813,10 @@ ] } ], - "source": ["# Final answer\nprint(step[END][-1].content)"] + "source": [ + "# Final answer\n", + "print(step[END][-1].content)" + ] }, { "cell_type": "markdown", @@ -440,7 +851,21 @@ ] } ], - "source": ["steps = chain.stream(\n [\n HumanMessage(\n content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n )\n ],\n {\n \"recursion_limit\": 100,\n },\n)\nfor step in steps:\n print(step)\n print(\"---\")"] + "source": [ + "steps = chain.stream(\n", + " [\n", + " HumanMessage(\n", + " content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n", + " )\n", + " ],\n", + " {\n", + " \"recursion_limit\": 100,\n", + " },\n", + ")\n", + "for step in steps:\n", + " print(step)\n", + " print(\"---\")" + ] }, { "cell_type": "code", @@ -456,7 +881,10 @@ ] } ], - "source": ["# Final answer\nprint(step[END][-1].content)"] + "source": [ + "# Final answer\n", + "print(step[END][-1].content)" + ] }, { "cell_type": "markdown", @@ -482,7 +910,16 @@ ] } ], - "source": ["for step in chain.stream(\n [\n HumanMessage(\n content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n )\n ]\n):\n print(step)"] + "source": [ + "for step in chain.stream(\n", + " [\n", + " HumanMessage(\n", + " content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n", + " )\n", + " ]\n", + "):\n", + " print(step)" + ] }, { "cell_type": "code", @@ -500,7 +937,10 @@ ] } ], - "source": ["# Final answer\nprint(step[END][-1].content)"] + "source": [ + "# Final answer\n", + "print(step[END][-1].content)" + ] }, { "cell_type": "markdown", @@ -522,7 +962,7 @@ "id": "431217e6-4c00-409f-a2bd-40ebff902489", "metadata": {}, "outputs": [], - "source": [""] + "source": [] } ], "metadata": { diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 72f3d1bc3..810990e38 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -32,7 +32,10 @@ "id": "8b323f43-328b-4b4b-88b0-6c84dc0a1d60", "metadata": {}, "outputs": [], - "source": ["%pip install -U --quiet langgraph langchain-fireworks\n%pip install -U --quiet tavily-python"] + "source": [ + "%pip install -U --quiet langgraph langchain-fireworks\n", + "%pip install -U --quiet tavily-python" + ] }, { "cell_type": "code", @@ -40,7 +43,24 @@ "id": "3368f330-cad6-4d35-a291-68fbf4389d98", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflection\"\n\n_set_if_undefined(\"FIREWORKS_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str) -> None:\n", + " if os.environ.get(var):\n", + " return\n", + " os.environ[var] = getpass.getpass(var)\n", + "\n", + "\n", + "# Optional: Configure tracing to visualize and debug the agent\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflection\"\n", + "\n", + "_set_if_undefined(\"FIREWORKS_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -58,7 +78,28 @@ "id": "cc10028f-9cef-4936-9419-cbdf06d24f1e", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_fireworks import ChatFireworks\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an essay assistant tasked with writing excellent 5-paragraph essays.\"\n \" Generate the best essay possible for the user's request.\"\n \" If the user provides critique, respond with a revised version of your previous attempts.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\nllm = ChatFireworks(\n model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n model_kwargs={\"max_tokens\": 32768},\n)\ngenerate = prompt | llm"] + "source": [ + "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_fireworks import ChatFireworks\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an essay assistant tasked with writing excellent 5-paragraph essays.\"\n", + " \" Generate the best essay possible for the user's request.\"\n", + " \" If the user provides critique, respond with a revised version of your previous attempts.\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + ")\n", + "llm = ChatFireworks(\n", + " model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n", + " model_kwargs={\"max_tokens\": 32768},\n", + ")\n", + "generate = prompt | llm" + ] }, { "cell_type": "code", @@ -86,7 +127,15 @@ ] } ], - "source": ["essay = \"\"\nrequest = HumanMessage(\n content=\"Write an essay on why the little prince is relevant in modern childhood\"\n)\nfor chunk in generate.stream({\"messages\": [request]}):\n print(chunk.content, end=\"\")\n essay += chunk.content"] + "source": [ + "essay = \"\"\n", + "request = HumanMessage(\n", + " content=\"Write an essay on why the little prince is relevant in modern childhood\"\n", + ")\n", + "for chunk in generate.stream({\"messages\": [request]}):\n", + " print(chunk.content, end=\"\")\n", + " essay += chunk.content" + ] }, { "cell_type": "markdown", @@ -102,7 +151,19 @@ "id": "a705be92-88c0-4f4f-b4c2-cdcd9af8cb2c", "metadata": {}, "outputs": [], - "source": ["reflection_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\"\n \" Provide detailed recommendations, including requests for length, depth, style, etc.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\nreflect = reflection_prompt | llm"] + "source": [ + "reflection_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\"\n", + " \" Provide detailed recommendations, including requests for length, depth, style, etc.\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " ]\n", + ")\n", + "reflect = reflection_prompt | llm" + ] }, { "cell_type": "code", @@ -132,7 +193,12 @@ ] } ], - "source": ["reflection = \"\"\nfor chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n print(chunk.content, end=\"\")\n reflection += chunk.content"] + "source": [ + "reflection = \"\"\n", + "for chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n", + " print(chunk.content, end=\"\")\n", + " reflection += chunk.content" + ] }, { "cell_type": "markdown", @@ -170,7 +236,12 @@ ] } ], - "source": ["for chunk in generate.stream(\n {\"messages\": [request, AIMessage(content=essay), HumanMessage(content=reflection)]}\n):\n print(chunk.content, end=\"\")"] + "source": [ + "for chunk in generate.stream(\n", + " {\"messages\": [request, AIMessage(content=essay), HumanMessage(content=reflection)]}\n", + "):\n", + " print(chunk.content, end=\"\")" + ] }, { "cell_type": "markdown", @@ -188,7 +259,50 @@ "id": "9e9a9d7c-5d2e-4194-b745-4511ec20db76", "metadata": {}, "outputs": [], - "source": ["from typing import List, Sequence\n\nfrom langgraph.graph import END, MessageGraph, START\n\n\nasync def generation_node(state: Sequence[BaseMessage]):\n return await generate.ainvoke({\"messages\": state})\n\n\nasync def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n # Other messages we need to adjust\n cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n # First message is the original user request. We hold it the same for all nodes\n translated = [messages[0]] + [\n cls_map[msg.type](content=msg.content) for msg in messages[1:]\n ]\n res = await reflect.ainvoke({\"messages\": translated})\n # We treat the output of this as human feedback for the generator\n return HumanMessage(content=res.content)\n\n\nbuilder = MessageGraph()\nbuilder.add_node(\"generate\", generation_node)\nbuilder.add_node(\"reflect\", reflection_node)\nbuilder.add_edge(START, \"generate\")\n\n\ndef should_continue(state: List[BaseMessage]):\n if len(state) > 6:\n # End after 3 iterations\n return END\n return \"reflect\"\n\n\nbuilder.add_conditional_edges(\"generate\", should_continue)\nbuilder.add_edge(\"reflect\", \"generate\")\ngraph = builder.compile()"] + "source": [ + "from typing import Annotated, List, Sequence\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.graph.message import add_messages\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + "\n", + " \n", + "async def generation_node(state: Sequence[BaseMessage]):\n", + " return await generate.ainvoke({\"messages\": state})\n", + "\n", + "\n", + "async def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n", + " # Other messages we need to adjust\n", + " cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n", + " # First message is the original user request. We hold it the same for all nodes\n", + " translated = [messages[0]] + [\n", + " cls_map[msg.type](content=msg.content) for msg in messages[1:]\n", + " ]\n", + " res = await reflect.ainvoke({\"messages\": translated})\n", + " # We treat the output of this as human feedback for the generator\n", + " return HumanMessage(content=res.content)\n", + "\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"generate\", generation_node)\n", + "builder.add_node(\"reflect\", reflection_node)\n", + "builder.add_edge(START, \"generate\")\n", + "\n", + "\n", + "def should_continue(state: List[BaseMessage]):\n", + " if len(state) > 6:\n", + " # End after 3 iterations\n", + " return END\n", + " return \"reflect\"\n", + "\n", + "\n", + "builder.add_conditional_edges(\"generate\", should_continue)\n", + "builder.add_edge(\"reflect\", \"generate\")\n", + "graph = builder.compile()" + ] }, { "cell_type": "code", @@ -219,7 +333,17 @@ ] } ], - "source": ["async for event in graph.astream(\n [\n HumanMessage(\n content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n )\n ],\n):\n print(event)\n print(\"---\")"] + "source": [ + "async for event in graph.astream(\n", + " [\n", + " HumanMessage(\n", + " content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n", + " )\n", + " ],\n", + "):\n", + " print(event)\n", + " print(\"---\")" + ] }, { "cell_type": "code", @@ -371,7 +495,9 @@ ] } ], - "source": ["ChatPromptTemplate.from_messages(event[END]).pretty_print()"] + "source": [ + "ChatPromptTemplate.from_messages(event[END]).pretty_print()" + ] }, { "cell_type": "markdown", @@ -389,7 +515,7 @@ "id": "7c0e3efd-7f54-410e-bd31-36185a46b9a8", "metadata": {}, "outputs": [], - "source": [""] + "source": [] } ], "metadata": { diff --git a/examples/reflexion/reflexion.ipynb b/examples/reflexion/reflexion.ipynb index 9dcbcc7c7..670e6eb5b 100644 --- a/examples/reflexion/reflexion.ipynb +++ b/examples/reflexion/reflexion.ipynb @@ -40,7 +40,10 @@ "id": "1b64a6f6-1d32-48be-92b5-66c3b04b17f7", "metadata": {}, "outputs": [], - "source": ["%pip install -U --quiet langgraph langchain_anthropic\n%pip install -U --quiet tavily-python"] + "source": [ + "%pip install -U --quiet langgraph langchain_anthropic\n", + "%pip install -U --quiet tavily-python" + ] }, { "cell_type": "code", @@ -48,7 +51,25 @@ "id": "a917bb70-f84c-48e6-8d32-d14f9df2ca2f", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n\n_set_if_undefined(\"ANTHROPIC_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str) -> None:\n", + " if os.environ.get(var):\n", + " return\n", + " os.environ[var] = getpass.getpass(var)\n", + "\n", + "\n", + "# Optional: Configure tracing to visualize and debug the agent\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n", + "\n", + "_set_if_undefined(\"ANTHROPIC_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")" + ] }, { "cell_type": "code", @@ -56,7 +77,15 @@ "id": "567b6c4a", "metadata": {}, "outputs": [], - "source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n# You could also use OpenAI or another provider\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"] + "source": [ + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n", + "# You could also use OpenAI or another provider\n", + "# from langchain_openai import ChatOpenAI\n", + "\n", + "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" + ] }, { "cell_type": "markdown", @@ -81,7 +110,13 @@ "id": "5a2ac853-b8a6-40de-b7fe-3f9f3c5ca4d2", "metadata": {}, "outputs": [], - "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"] + "source": [ + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n", + "\n", + "search = TavilySearchAPIWrapper()\n", + "tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)" + ] }, { "cell_type": "markdown", @@ -97,7 +132,54 @@ "id": "5fffa8d5-068a-4f0b-adfc-b4daf30ef294", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import HumanMessage, ToolMessage\nfrom langchain_core.output_parsers.openai_tools import PydanticToolsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n\n\nclass Reflection(BaseModel):\n missing: str = Field(description=\"Critique of what is missing.\")\n superfluous: str = Field(description=\"Critique of what is superfluous\")\n\n\nclass AnswerQuestion(BaseModel):\n \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n\n answer: str = Field(description=\"~250 word detailed answer to the question.\")\n reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n search_queries: list[str] = Field(\n description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n )\n\n\nclass ResponderWithRetries:\n def __init__(self, runnable, validator):\n self.runnable = runnable\n self.validator = validator\n\n def respond(self, state: list):\n response = []\n for attempt in range(3):\n response = self.runnable.invoke(\n {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n )\n try:\n self.validator.invoke(response)\n return response\n except ValidationError as e:\n state = state + [\n response,\n ToolMessage(\n content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n + self.validator.schema_json()\n + \" Respond by fixing all validation errors.\",\n tool_call_id=response.tool_calls[0][\"id\"],\n ),\n ]\n return response"] + "source": [ + "from langchain_core.messages import HumanMessage, ToolMessage\n", + "from langchain_core.output_parsers.openai_tools import PydanticToolsParser\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n", + "\n", + "\n", + "class Reflection(BaseModel):\n", + " missing: str = Field(description=\"Critique of what is missing.\")\n", + " superfluous: str = Field(description=\"Critique of what is superfluous\")\n", + "\n", + "\n", + "class AnswerQuestion(BaseModel):\n", + " \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n", + "\n", + " answer: str = Field(description=\"~250 word detailed answer to the question.\")\n", + " reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n", + " search_queries: list[str] = Field(\n", + " description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n", + " )\n", + "\n", + "\n", + "class ResponderWithRetries:\n", + " def __init__(self, runnable, validator):\n", + " self.runnable = runnable\n", + " self.validator = validator\n", + "\n", + " def respond(self, state: list):\n", + " response = []\n", + " for attempt in range(3):\n", + " response = self.runnable.invoke(\n", + " {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n", + " )\n", + " try:\n", + " self.validator.invoke(response)\n", + " return response\n", + " except ValidationError as e:\n", + " state = state + [\n", + " response,\n", + " ToolMessage(\n", + " content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n", + " + self.validator.schema_json()\n", + " + \" Respond by fixing all validation errors.\",\n", + " tool_call_id=response.tool_calls[0][\"id\"],\n", + " ),\n", + " ]\n", + " return response" + ] }, { "cell_type": "code", @@ -114,7 +196,40 @@ ] } ], - "source": ["import datetime\n\nactor_prompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are expert researcher.\nCurrent time: {time}\n\n1. {first_instruction}\n2. Reflect and critique your answer. Be severe to maximize improvement.\n3. Recommend search queries to research information and improve your answer.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"user\",\n \"\\n\\nReflect on the user's original question and the\"\n \" actions taken thus far. Respond using the {function_name} function.\",\n ),\n ]\n).partial(\n time=lambda: datetime.datetime.now().isoformat(),\n)\ninitial_answer_chain = actor_prompt_template.partial(\n first_instruction=\"Provide a detailed ~250 word answer.\",\n function_name=AnswerQuestion.__name__,\n) | llm.bind_tools(tools=[AnswerQuestion])\nvalidator = PydanticToolsParser(tools=[AnswerQuestion])\n\nfirst_responder = ResponderWithRetries(\n runnable=initial_answer_chain, validator=validator\n)"] + "source": [ + "import datetime\n", + "\n", + "actor_prompt_template = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are expert researcher.\n", + "Current time: {time}\n", + "\n", + "1. {first_instruction}\n", + "2. Reflect and critique your answer. Be severe to maximize improvement.\n", + "3. Recommend search queries to research information and improve your answer.\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"user\",\n", + " \"\\n\\nReflect on the user's original question and the\"\n", + " \" actions taken thus far. Respond using the {function_name} function.\",\n", + " ),\n", + " ]\n", + ").partial(\n", + " time=lambda: datetime.datetime.now().isoformat(),\n", + ")\n", + "initial_answer_chain = actor_prompt_template.partial(\n", + " first_instruction=\"Provide a detailed ~250 word answer.\",\n", + " function_name=AnswerQuestion.__name__,\n", + ") | llm.bind_tools(tools=[AnswerQuestion])\n", + "validator = PydanticToolsParser(tools=[AnswerQuestion])\n", + "\n", + "first_responder = ResponderWithRetries(\n", + " runnable=initial_answer_chain, validator=validator\n", + ")" + ] }, { "cell_type": "code", @@ -122,7 +237,10 @@ "id": "5922e1fe-7533-4f41-8b1d-d812707c1968", "metadata": {}, "outputs": [], - "source": ["example_question = \"Why is reflection useful in AI?\"\ninitial = first_responder.respond([HumanMessage(content=example_question)])"] + "source": [ + "example_question = \"Why is reflection useful in AI?\"\n", + "initial = first_responder.respond([HumanMessage(content=example_question)])" + ] }, { "cell_type": "markdown", @@ -140,7 +258,38 @@ "id": "2605fd8d-c663-446f-ba25-751190195749", "metadata": {}, "outputs": [], - "source": ["revise_instructions = \"\"\"Revise your previous answer using the new information.\n - You should use the previous critique to add important information to your answer.\n - You MUST include numerical citations in your revised answer to ensure it can be verified.\n - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n - [1] https://example.com\n - [2] https://example.com\n - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n\"\"\"\n\n\n# Extend the initial answer schema to include references.\n# Forcing citation in the model encourages grounded responses\nclass ReviseAnswer(AnswerQuestion):\n \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n\n cite your reflection with references, and finally\n add search queries to improve the answer.\"\"\"\n\n references: list[str] = Field(\n description=\"Citations motivating your updated answer.\"\n )\n\n\nrevision_chain = actor_prompt_template.partial(\n first_instruction=revise_instructions,\n function_name=ReviseAnswer.__name__,\n) | llm.bind_tools(tools=[ReviseAnswer])\nrevision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n\nrevisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"] + "source": [ + "revise_instructions = \"\"\"Revise your previous answer using the new information.\n", + " - You should use the previous critique to add important information to your answer.\n", + " - You MUST include numerical citations in your revised answer to ensure it can be verified.\n", + " - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n", + " - [1] https://example.com\n", + " - [2] https://example.com\n", + " - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n", + "\"\"\"\n", + "\n", + "\n", + "# Extend the initial answer schema to include references.\n", + "# Forcing citation in the model encourages grounded responses\n", + "class ReviseAnswer(AnswerQuestion):\n", + " \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n", + "\n", + " cite your reflection with references, and finally\n", + " add search queries to improve the answer.\"\"\"\n", + "\n", + " references: list[str] = Field(\n", + " description=\"Citations motivating your updated answer.\"\n", + " )\n", + "\n", + "\n", + "revision_chain = actor_prompt_template.partial(\n", + " first_instruction=revise_instructions,\n", + " function_name=ReviseAnswer.__name__,\n", + ") | llm.bind_tools(tools=[ReviseAnswer])\n", + "revision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n", + "\n", + "revisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)" + ] }, { "cell_type": "code", @@ -159,7 +308,25 @@ "output_type": "execute_result" } ], - "source": ["import json\n\nrevised = revisor.respond(\n [\n HumanMessage(content=example_question),\n initial,\n ToolMessage(\n tool_call_id=initial.tool_calls[0][\"id\"],\n content=json.dumps(\n tavily_tool.invoke(\n {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n )\n ),\n ),\n ]\n)\nrevised"] + "source": [ + "import json\n", + "\n", + "revised = revisor.respond(\n", + " [\n", + " HumanMessage(content=example_question),\n", + " initial,\n", + " ToolMessage(\n", + " tool_call_id=initial.tool_calls[0][\"id\"],\n", + " content=json.dumps(\n", + " tavily_tool.invoke(\n", + " {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n", + " )\n", + " ),\n", + " ),\n", + " ]\n", + ")\n", + "revised" + ] }, { "cell_type": "markdown", @@ -177,7 +344,24 @@ "id": "fccd6a17", "metadata": {}, "outputs": [], - "source": ["from langchain_core.tools import StructuredTool\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef run_queries(search_queries: list[str], **kwargs):\n \"\"\"Run the generated queries.\"\"\"\n return tavily_tool.batch([{\"query\": query} for query in search_queries])\n\n\ntool_node = ToolNode(\n [\n StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n ]\n)"] + "source": [ + "from langchain_core.tools import StructuredTool\n", + "\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "\n", + "def run_queries(search_queries: list[str], **kwargs):\n", + " \"\"\"Run the generated queries.\"\"\"\n", + " return tavily_tool.batch([{\"query\": query} for query in search_queries])\n", + "\n", + "\n", + "tool_node = ToolNode(\n", + " [\n", + " StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n", + " StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n", + " ]\n", + ")" + ] }, { "cell_type": "markdown", @@ -196,7 +380,55 @@ "id": "3c57318f-a30c-4dbd-9b88-f2633e8cb3b1", "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\nfrom langgraph.graph import END, MessageGraph, START\n\nMAX_ITERATIONS = 5\nbuilder = MessageGraph()\nbuilder.add_node(\"draft\", first_responder.respond)\n\n\nbuilder.add_node(\"execute_tools\", tool_node)\nbuilder.add_node(\"revise\", revisor.respond)\n# draft -> execute_tools\nbuilder.add_edge(\"draft\", \"execute_tools\")\n# execute_tools -> revise\nbuilder.add_edge(\"execute_tools\", \"revise\")\n\n# Define looping logic:\n\n\ndef _get_num_iterations(state: list):\n i = 0\n for m in state[::-1]:\n if m.type not in {\"tool\", \"ai\"}:\n break\n i += 1\n return i\n\n\ndef event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n # in our case, we'll just stop after N plans\n num_iterations = _get_num_iterations(state)\n if num_iterations > MAX_ITERATIONS:\n return END\n return \"execute_tools\"\n\n\n# revise -> execute_tools OR end\nbuilder.add_conditional_edges(\"revise\", event_loop)\nbuilder.add_edge(START, \"draft\")\ngraph = builder.compile()"] + "source": [ + "from typing import Literal\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.graph.message import add_messages\n", + "from typing import Annotated\n", + "from typing_extensions import TypedDict\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]\n", + "\n", + "MAX_ITERATIONS = 5\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"draft\", first_responder.respond)\n", + "\n", + "\n", + "builder.add_node(\"execute_tools\", tool_node)\n", + "builder.add_node(\"revise\", revisor.respond)\n", + "# draft -> execute_tools\n", + "builder.add_edge(\"draft\", \"execute_tools\")\n", + "# execute_tools -> revise\n", + "builder.add_edge(\"execute_tools\", \"revise\")\n", + "\n", + "# Define looping logic:\n", + "\n", + "\n", + "def _get_num_iterations(state: list):\n", + " i = 0\n", + " for m in state[::-1]:\n", + " if m.type not in {\"tool\", \"ai\"}:\n", + " break\n", + " i += 1\n", + " return i\n", + "\n", + "\n", + "def event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n", + " # in our case, we'll just stop after N plans\n", + " num_iterations = _get_num_iterations(state)\n", + " if num_iterations > MAX_ITERATIONS:\n", + " return END\n", + " return \"execute_tools\"\n", + "\n", + "\n", + "# revise -> execute_tools OR end\n", + "builder.add_conditional_edges(\"revise\", event_loop)\n", + "builder.add_edge(START, \"draft\")\n", + "graph = builder.compile()" + ] }, { "cell_type": "code", @@ -215,7 +447,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", @@ -330,7 +570,15 @@ ] } ], - "source": ["events = graph.stream(\n [HumanMessage(content=\"How should we handle the climate crisis?\")],\n stream_mode=\"values\",\n)\nfor i, step in enumerate(events):\n print(f\"Step {i}\")\n step[-1].pretty_print()"] + "source": [ + "events = graph.stream(\n", + " [HumanMessage(content=\"How should we handle the climate crisis?\")],\n", + " stream_mode=\"values\",\n", + ")\n", + "for i, step in enumerate(events):\n", + " print(f\"Step {i}\")\n", + " step[-1].pretty_print()" + ] }, { "cell_type": "markdown", diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index 24622a4a7..ed7b69553 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -221,28 +221,34 @@ def tools_condition( ```pycon >>> from langchain_anthropic import ChatAnthropic >>> from langchain_core.tools import tool - >>> - >>> from langgraph.graph import MessageGraph + ... + >>> from langgraph.graph import StateGraph >>> from langgraph.prebuilt import ToolNode, tools_condition - >>> + >>> from langgraph.graph.message import add_messages + ... + >>> from typing import TypedDict, Annotated + ... >>> @tool >>> def divide(a: float, b: float) -> int: - >>> \"\"\"Return a / b.\"\"\" - >>> return a / b - >>> + ... \"\"\"Return a / b.\"\"\" + ... return a / b + ... >>> llm = ChatAnthropic(model="claude-3-haiku-20240307") >>> tools = [divide] + ... + >>> class State(TypedDict): + ... messages: Annotated[list, add_messages] >>> - >>> graph_builder = MessageGraph() + >>> graph_builder = StateGraph(State) >>> graph_builder.add_node("tools", ToolNode(tools)) - >>> graph_builder.add_node("chatbot", llm.bind_tools(tools)) + >>> graph_builder.add_node("chatbot", lambda state: {"messages":llm.bind_tools(tools).invoke(state['messages'])}) >>> graph_builder.add_edge("tools", "chatbot") >>> graph_builder.add_conditional_edges( ... "chatbot", tools_condition ... ) >>> graph_builder.set_entry_point("chatbot") >>> graph = graph_builder.compile() - >>> graph.invoke([("user", "What's 329993 divided by 13662?")]) + >>> graph.invoke({"messages": {"role": "user", "content": "What's 329993 divided by 13662?"}}) ``` """ if isinstance(state, list): diff --git a/libs/langgraph/langgraph/prebuilt/tool_validator.py b/libs/langgraph/langgraph/prebuilt/tool_validator.py index 5f54398f4..73b2cce2b 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_validator.py +++ b/libs/langgraph/langgraph/prebuilt/tool_validator.py @@ -72,13 +72,14 @@ class ValidationNode(RunnableCallable): Examples: Example usage for re-prompting the model to generate a valid response: - >>> from typing import Literal + >>> from typing import Literal, Annotated, TypedDict ... >>> from langchain_anthropic import ChatAnthropic >>> from langchain_core.pydantic_v1 import BaseModel, validator ... - >>> from langgraph.graph import END, START, MessageGraph + >>> from langgraph.graph import END, START, StateGraph >>> from langgraph.prebuilt import ValidationNode + >>> from langgraph.graph.message import add_messages ... ... >>> class SelectNumber(BaseModel): @@ -91,7 +92,10 @@ class ValidationNode(RunnableCallable): ... return v ... ... - >>> builder = MessageGraph() + >>> class State(TypedDict): + ... messages: Annotated[list, add_messages] + ... + >>> builder = StateGraph(State) >>> llm = ChatAnthropic(model="claude-3-haiku-20240307").bind_tools([SelectNumber]) >>> builder.add_node("model", llm) >>> builder.add_node("validation", ValidationNode([SelectNumber]))