From 727e63c01efbde33706119ebc65390fd66ce6cd3 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Mon, 1 Jul 2024 21:36:34 -0700 Subject: [PATCH] [Docs] Update notebooks to use START (#902) --- docs/docs/concepts/low_level.md | 17 +- examples/agent_executor/base.ipynb | 149 +- .../force-calling-a-tool-first.ipynb | 173 +- .../agent_executor/human-in-the-loop.ipynb | 153 +- .../agent_executor/managing-agent-steps.ipynb | 153 +- examples/async.ipynb | 180 +- examples/branching.ipynb | 250 +- .../anthropic.ipynb | 136 +- .../base.ipynb | 192 +- .../dynamically-returning-directly.ipynb | 221 +- .../force-calling-a-tool-first.ipynb | 224 +- .../human-in-the-loop.ipynb | 196 +- .../managing-agent-steps.ipynb | 186 +- .../prebuilt-tool-node.ipynb | 154 +- .../respond-in-format.ipynb | 245 +- .../agent-simulation-evaluation.ipynb | 160 +- .../simulation_utils.py | 4 +- .../langgraph_code_assistant.ipynb | 504 +--- .../langgraph_code_assistant_mistral.ipynb | 411 +--- examples/configuration.ipynb | 104 +- .../customer-support/customer-support.ipynb | 2115 +---------------- examples/docs/quickstart.ipynb | 103 +- examples/dynamically-returning-directly.ipynb | 219 +- examples/extraction/retries.ipynb | 730 +----- examples/force-calling-a-tool-first.ipynb | 215 +- examples/human-in-the-loop.ipynb | 353 +-- examples/human_in_the_loop/breakpoints.ipynb | 135 +- .../human_in_the_loop/edit-graph-state.ipynb | 162 +- examples/human_in_the_loop/time-travel.ipynb | 204 +- .../human_in_the_loop/wait-user-input.ipynb | 202 +- examples/introduction.ipynb | 948 +------- examples/lats/lats.ipynb | 458 +--- examples/learning.ipynb | 242 +- examples/llm-compiler/LLMCompiler.ipynb | 483 +--- examples/managing-agent-steps.ipynb | 172 +- examples/managing-conversation-history.ipynb | 8 +- examples/map-reduce.ipynb | 107 +- examples/multi_agent/agent_supervisor.ipynb | 195 +- .../hierarchical_agent_teams.ipynb | 547 +---- .../multi-agent-collaboration.ipynb | 232 +- examples/pass-run-time-values-to-tools.ipynb | 246 +- examples/persistence.ipynb | 184 +- .../plan-and-execute/plan-and-execute.ipynb | 234 +- examples/rag/langgraph_adaptive_rag.ipynb | 567 +---- .../rag/langgraph_adaptive_rag_cohere.ipynb | 614 +---- .../rag/langgraph_adaptive_rag_local.ipynb | 498 +--- examples/rag/langgraph_agentic_rag.ipynb | 317 +-- examples/rag/langgraph_crag.ipynb | 385 +-- examples/rag/langgraph_crag_local.ipynb | 474 +--- .../langgraph_rag_agent_llama3_local.ipynb | 495 +--- examples/rag/langgraph_self_rag.ipynb | 450 +--- examples/rag/langgraph_self_rag_local.ipynb | 404 +--- .../langgraph_self_rag_pinecone_movies.ipynb | 412 +--- examples/reflection/reflection.ipynb | 143 +- examples/reflexion/reflexion.ipynb | 267 +-- examples/respond-in-format.ipynb | 169 +- examples/rewoo/rewoo.ipynb | 194 +- examples/state-context-key.ipynb | 4 +- examples/state-model.ipynb | 175 +- examples/storm/storm.ipynb | 864 +------ examples/streaming-from-final-node.ipynb | 138 +- examples/subgraph.ipynb | 165 +- examples/time-travel.ipynb | 270 +-- examples/tutorials/sql-agent.ipynb | 493 +--- examples/tutorials/tnt-llm/tnt-llm.ipynb | 611 +---- examples/usaco/usaco.ipynb | 773 +----- examples/visualization.ipynb | 129 +- 67 files changed, 1059 insertions(+), 20258 deletions(-) diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index f3c49d381..66b0efacd 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -193,13 +193,7 @@ graph.add_edge("node_a", routing_function, {True: "node_b", False: "node_c"}) ### Entry Point -The entry point is first node to call when the graph starts. You can use [`set_entry_point`][langgraph.graph.StateGraph.set_entry_point] to specify this. - -```python -graph.set_entry_point("node_a") -``` - -This is equivalent to adding an edge between the `START` node and this node. You may want to use `START` directly when you want to have **multiple** nodes be called first. +The entry point is the first node(s) that are run when the graph starts. You can use the [`add_edge`][langgraph.graph.StateGraph.add_edge] method from the virtual [`START`][start] node to the first node to execute to specify where to enter the graph. ```python from langgraph.graph import START @@ -209,17 +203,18 @@ graph.add_edge(START, "node_a") ### Conditional Entry Point -The conditional entry point is used when you want to specify a function to call to determine which node(s) should be called first. -You can use [`set_conditional_entry_point`][langgraph.graph.StateGraph.set_conditional_entry_point] to specify this. +A conditional entry point lets you start at different nodes depending on custom logic. You can use [`add_conditional_edges`][langgraph.graph.StateGraph.add_conditional_edges] from the virtual [`START`][start] node to accomplish this. ```python -graph.set_conditional_entry_point(routing_function) +from langgraph.graph import START + +graph.add_conditional_edges(START, routing_function) ``` You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node. ```python -graph.set_conditional_entry_point(routing_function, {True: "node_b", False: "node_c"}) +graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"}) ``` ## `Send` diff --git a/examples/agent_executor/base.ipynb b/examples/agent_executor/base.ipynb index 31aad70a5..f9d9cf8ba 100644 --- a/examples/agent_executor/base.ipynb +++ b/examples/agent_executor/base.ipynb @@ -25,10 +25,7 @@ "id": "fdd4ce41-4152-423b-b3f7-be3b4d568cf4", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai langchainhub tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai langchainhub tavily-python"] }, { "cell_type": "markdown", @@ -44,13 +41,7 @@ "id": "6398c4c1-da78-4595-8a5a-051ed2d1de72", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -66,10 +57,7 @@ "id": "dcbf79ad-4de5-43b0-a3a1-25b33711e46c", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -87,23 +75,7 @@ "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_openai.chat_models import ChatOpenAI\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]\n", - "\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", - "\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n", - "\n", - "# Construct the OpenAI Functions agent\n", - "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" - ] + "source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"] }, { "cell_type": "markdown", @@ -126,27 +98,7 @@ "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Union\n", - "\n", - "from langchain_core.agents import AgentAction, AgentFinish\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " # The input string\n", - " input: str\n", - " # The list of previous messages in the conversation\n", - " chat_history: list[BaseMessage]\n", - " # The outcome of a given call to the agent\n", - " # Needs `None` as a valid type, since this is what this will start as\n", - " agent_outcome: Union[AgentAction, AgentFinish, None]\n", - " # List of actions and corresponding observations\n", - " # Here we annotate this with `operator.add` to indicate that operations to\n", - " # this state should be ADDED to the existing values (not overwrite it)\n", - " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"] }, { "cell_type": "markdown", @@ -181,42 +133,7 @@ "id": "d61a970d-edf4-4eef-9678-28bab7c72331", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.agents import AgentFinish\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "\n", - "# This a helper class we have that is useful for running tools\n", - "# It takes in an agent action and calls that tool and returns the result\n", - "tool_executor = ToolExecutor(tools)\n", - "\n", - "\n", - "# Define the agent\n", - "def run_agent(data):\n", - " agent_outcome = agent_runnable.invoke(data)\n", - " return {\"agent_outcome\": agent_outcome}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def execute_tools(data):\n", - " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", - " agent_action = data[\"agent_outcome\"]\n", - " output = tool_executor.invoke(agent_action)\n", - " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", - "\n", - "\n", - "# Define logic that will be used to determine which conditional edge to go down\n", - "def should_continue(data):\n", - " # If the agent outcome is an AgentFinish, then we return `exit` string\n", - " # This will be used when setting up the graph to define the flow\n", - " if isinstance(data[\"agent_outcome\"], AgentFinish):\n", - " return \"end\"\n", - " # Otherwise, an AgentAction is returned\n", - " # Here we return `continue` string\n", - " # This will be used when setting up the graph to define the flow\n", - " else:\n", - " return \"continue\"" - ] + "source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)\n\n\n# Define the agent\ndef run_agent(data):\n agent_outcome = agent_runnable.invoke(data)\n return {\"agent_outcome\": agent_outcome}\n\n\n# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -234,50 +151,7 @@ "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", run_agent)\n", - "workflow.add_node(\"action\", execute_tools)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -300,12 +174,7 @@ ] } ], - "source": [ - "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] + "source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"] }, { "cell_type": "code", @@ -313,7 +182,7 @@ "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/agent_executor/force-calling-a-tool-first.ipynb b/examples/agent_executor/force-calling-a-tool-first.ipynb index e2d464dcf..11bbe1c31 100644 --- a/examples/agent_executor/force-calling-a-tool-first.ipynb +++ b/examples/agent_executor/force-calling-a-tool-first.ipynb @@ -30,10 +30,7 @@ "id": "694cfc4c-22a7-495d-930d-56b21d850ff9", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -49,13 +46,7 @@ "id": "30c06a84-291a-4f58-9d31-53d3b56a3def", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -71,10 +62,7 @@ "id": "a8fb285a-7e6e-46fc-a273-43ab1a676189", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -92,23 +80,7 @@ "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_openai.chat_models import ChatOpenAI\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]\n", - "\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", - "\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n", - "\n", - "# Construct the OpenAI Functions agent\n", - "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" - ] + "source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"] }, { "cell_type": "markdown", @@ -131,27 +103,7 @@ "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Union\n", - "\n", - "from langchain_core.agents import AgentAction, AgentFinish\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " # The input string\n", - " input: str\n", - " # The list of previous messages in the conversation\n", - " chat_history: list[BaseMessage]\n", - " # The outcome of a given call to the agent\n", - " # Needs `None` as a valid type, since this is what this will start as\n", - " agent_outcome: Union[AgentAction, AgentFinish, None]\n", - " # List of actions and corresponding observations\n", - " # Here we annotate this with `operator.add` to indicate that operations to\n", - " # this state should be ADDED to the existing values (not overwrite it)\n", - " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"] }, { "cell_type": "markdown", @@ -186,42 +138,7 @@ "id": "d61a970d-edf4-4eef-9678-28bab7c72331", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.agents import AgentFinish\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "\n", - "# This a helper class we have that is useful for running tools\n", - "# It takes in an agent action and calls that tool and returns the result\n", - "tool_executor = ToolExecutor(tools)\n", - "\n", - "\n", - "# Define the agent\n", - "def run_agent(data):\n", - " agent_outcome = agent_runnable.invoke(data)\n", - " return {\"agent_outcome\": agent_outcome}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def execute_tools(data):\n", - " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", - " agent_action = data[\"agent_outcome\"]\n", - " output = tool_executor.invoke(agent_action)\n", - " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", - "\n", - "\n", - "# Define logic that will be used to determine which conditional edge to go down\n", - "def should_continue(data):\n", - " # If the agent outcome is an AgentFinish, then we return `exit` string\n", - " # This will be used when setting up the graph to define the flow\n", - " if isinstance(data[\"agent_outcome\"], AgentFinish):\n", - " return \"end\"\n", - " # Otherwise, an AgentAction is returned\n", - " # Here we return `continue` string\n", - " # This will be used when setting up the graph to define the flow\n", - " else:\n", - " return \"continue\"" - ] + "source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)\n\n\n# Define the agent\ndef run_agent(data):\n agent_outcome = agent_runnable.invoke(data)\n return {\"agent_outcome\": agent_outcome}\n\n\n# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -250,9 +167,7 @@ "output_type": "execute_result" } ], - "source": [ - "tools[0].name" - ] + "source": ["tools[0].name"] }, { "cell_type": "code", @@ -260,21 +175,7 @@ "id": "df25d899-2338-4f31-a8bf-0582a2eec325", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.agents import AgentActionMessageLog\n", - "\n", - "\n", - "def first_agent(inputs):\n", - " action = AgentActionMessageLog(\n", - " # We force call this tool\n", - " tool=\"tavily_search_results_json\",\n", - " # We just pass in the `input` key to this tool\n", - " tool_input=inputs[\"input\"],\n", - " log=\"\",\n", - " message_log=[],\n", - " )\n", - " return {\"agent_outcome\": action}" - ] + "source": ["from langchain_core.agents import AgentActionMessageLog\n\n\ndef first_agent(inputs):\n action = AgentActionMessageLog(\n # We force call this tool\n tool=\"tavily_search_results_json\",\n # We just pass in the `input` key to this tool\n tool_input=inputs[\"input\"],\n log=\"\",\n message_log=[],\n )\n return {\"agent_outcome\": action}"] }, { "cell_type": "markdown", @@ -296,54 +197,7 @@ "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", run_agent)\n", - "workflow.add_node(\"action\", execute_tools)\n", - "workflow.add_node(\"first_agent\", first_agent)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"first_agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# After the first agent, we want to take an action\n", - "workflow.add_edge(\"first_agent\", \"action\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\nworkflow.add_node(\"first_agent\", first_agent)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"first_agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After the first agent, we want to take an action\nworkflow.add_edge(\"first_agent\", \"action\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -366,12 +220,7 @@ ] } ], - "source": [ - "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] + "source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"] }, { "cell_type": "code", @@ -379,7 +228,7 @@ "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/agent_executor/human-in-the-loop.ipynb b/examples/agent_executor/human-in-the-loop.ipynb index f53930192..f422374e8 100644 --- a/examples/agent_executor/human-in-the-loop.ipynb +++ b/examples/agent_executor/human-in-the-loop.ipynb @@ -30,10 +30,7 @@ "id": "3fa9e224-2f00-49e2-bca3-e9cb8d9f3d41", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -49,13 +46,7 @@ "id": "d180f0d0-385f-4ce3-994c-11e1d64595b5", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -71,10 +62,7 @@ "id": "72ad0539-ecd8-4eb1-b2c1-2242e5fc556f", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -92,23 +80,7 @@ "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_openai.chat_models import ChatOpenAI\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]\n", - "\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", - "\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n", - "\n", - "# Construct the OpenAI Functions agent\n", - "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" - ] + "source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"] }, { "cell_type": "markdown", @@ -131,27 +103,7 @@ "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Union\n", - "\n", - "from langchain_core.agents import AgentAction, AgentFinish\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " # The input string\n", - " input: str\n", - " # The list of previous messages in the conversation\n", - " chat_history: list[BaseMessage]\n", - " # The outcome of a given call to the agent\n", - " # Needs `None` as a valid type, since this is what this will start as\n", - " agent_outcome: Union[AgentAction, AgentFinish, None]\n", - " # List of actions and corresponding observations\n", - " # Here we annotate this with `operator.add` to indicate that operations to\n", - " # this state should be ADDED to the existing values (not overwrite it)\n", - " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"] }, { "cell_type": "markdown", @@ -186,21 +138,7 @@ "id": "2b757f84-1175-445e-8f8c-e5aeb765a03d", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.agents import AgentFinish\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "\n", - "# This a helper class we have that is useful for running tools\n", - "# It takes in an agent action and calls that tool and returns the result\n", - "tool_executor = ToolExecutor(tools)\n", - "\n", - "\n", - "# Define the agent\n", - "def run_agent(data):\n", - " agent_outcome = agent_runnable.invoke(data)\n", - " return {\"agent_outcome\": agent_outcome}" - ] + "source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)\n\n\n# Define the agent\ndef run_agent(data):\n agent_outcome = agent_runnable.invoke(data)\n return {\"agent_outcome\": agent_outcome}"] }, { "cell_type": "markdown", @@ -218,30 +156,7 @@ "id": "2fecf5e0-9604-4992-9c82-b9627466cd32", "metadata": {}, "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def execute_tools(data):\n", - " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", - " agent_action = data[\"agent_outcome\"]\n", - " response = input(prompt=f\"[y/n] continue with: {agent_action}?\")\n", - " if response == \"n\":\n", - " raise ValueError\n", - " output = tool_executor.invoke(agent_action)\n", - " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", - "\n", - "\n", - "# Define logic that will be used to determine which conditional edge to go down\n", - "def should_continue(data):\n", - " # If the agent outcome is an AgentFinish, then we return `exit` string\n", - " # This will be used when setting up the graph to define the flow\n", - " if isinstance(data[\"agent_outcome\"], AgentFinish):\n", - " return \"end\"\n", - " # Otherwise, an AgentAction is returned\n", - " # Here we return `continue` string\n", - " # This will be used when setting up the graph to define the flow\n", - " else:\n", - " return \"continue\"" - ] + "source": ["# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n response = input(prompt=f\"[y/n] continue with: {agent_action}?\")\n if response == \"n\":\n raise ValueError\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -259,50 +174,7 @@ "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", run_agent)\n", - "workflow.add_node(\"action\", execute_tools)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -338,12 +210,7 @@ ] } ], - "source": [ - "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] + "source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"] }, { "cell_type": "code", @@ -351,7 +218,7 @@ "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/agent_executor/managing-agent-steps.ipynb b/examples/agent_executor/managing-agent-steps.ipynb index 24b10d9c1..4fced90d3 100644 --- a/examples/agent_executor/managing-agent-steps.ipynb +++ b/examples/agent_executor/managing-agent-steps.ipynb @@ -30,10 +30,7 @@ "id": "aa752131-27e3-4bd8-9f21-d6749a7e74f4", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -49,13 +46,7 @@ "id": "5732e68f-4ae2-4db9-bf9c-454b4cc9ec01", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -71,10 +62,7 @@ "id": "652d4600-8f95-493f-b9b9-d4095aed9218", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -92,23 +80,7 @@ "id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain.agents import create_openai_functions_agent\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_openai.chat_models import ChatOpenAI\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]\n", - "\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n", - "\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n", - "\n", - "# Construct the OpenAI Functions agent\n", - "agent_runnable = create_openai_functions_agent(llm, tools, prompt)" - ] + "source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"] }, { "cell_type": "markdown", @@ -131,27 +103,7 @@ "id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Union\n", - "\n", - "from langchain_core.agents import AgentAction, AgentFinish\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " # The input string\n", - " input: str\n", - " # The list of previous messages in the conversation\n", - " chat_history: list[BaseMessage]\n", - " # The outcome of a given call to the agent\n", - " # Needs `None` as a valid type, since this is what this will start as\n", - " agent_outcome: Union[AgentAction, AgentFinish, None]\n", - " # List of actions and corresponding observations\n", - " # Here we annotate this with `operator.add` to indicate that operations to\n", - " # this state should be ADDED to the existing values (not overwrite it)\n", - " intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"] }, { "cell_type": "markdown", @@ -186,15 +138,7 @@ "id": "77e3c059-e31f-4c8f-81bf-edb58688e12b", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.agents import AgentFinish\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor\n", - "\n", - "# This a helper class we have that is useful for running tools\n", - "# It takes in an agent action and calls that tool and returns the result\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -212,36 +156,7 @@ "id": "a9f66a3e-aba1-4893-95b1-a433c7091d5e", "metadata": {}, "outputs": [], - "source": [ - "# Define the agent\n", - "def run_agent(data):\n", - " inputs = data.copy()\n", - " if len(inputs[\"intermediate_steps\"]) > 5:\n", - " inputs[\"intermediate_steps\"] = inputs[\"intermediate_steps\"][-5:]\n", - " agent_outcome = agent_runnable.invoke(inputs)\n", - " return {\"agent_outcome\": agent_outcome}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def execute_tools(data):\n", - " # Get the most recent agent_outcome - this is the key added in the `agent` above\n", - " agent_action = data[\"agent_outcome\"]\n", - " output = tool_executor.invoke(agent_action)\n", - " return {\"intermediate_steps\": [(agent_action, str(output))]}\n", - "\n", - "\n", - "# Define logic that will be used to determine which conditional edge to go down\n", - "def should_continue(data):\n", - " # If the agent outcome is an AgentFinish, then we return `exit` string\n", - " # This will be used when setting up the graph to define the flow\n", - " if isinstance(data[\"agent_outcome\"], AgentFinish):\n", - " return \"end\"\n", - " # Otherwise, an AgentAction is returned\n", - " # Here we return `continue` string\n", - " # This will be used when setting up the graph to define the flow\n", - " else:\n", - " return \"continue\"" - ] + "source": ["# Define the agent\ndef run_agent(data):\n inputs = data.copy()\n if len(inputs[\"intermediate_steps\"]) > 5:\n inputs[\"intermediate_steps\"] = inputs[\"intermediate_steps\"][-5:]\n agent_outcome = agent_runnable.invoke(inputs)\n return {\"agent_outcome\": agent_outcome}\n\n\n# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -259,50 +174,7 @@ "id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", run_agent)\n", - "workflow.add_node(\"action\", execute_tools)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -325,12 +197,7 @@ ] } ], - "source": [ - "inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n", - "for s in app.stream(inputs):\n", - " print(list(s.values())[0])\n", - " print(\"----\")" - ] + "source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"] }, { "cell_type": "code", @@ -338,7 +205,7 @@ "id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/async.ipynb b/examples/async.ipynb index dd77cbd9a..fd6a1f8c1 100644 --- a/examples/async.ipynb +++ b/examples/async.ipynb @@ -37,10 +37,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] }, { "cell_type": "markdown", @@ -56,18 +53,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -83,10 +69,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -112,22 +95,7 @@ "id": "6768a3ab", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# Add messages essentially does this with more\n", - "# robust handling\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -147,19 +115,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder, but don't tell the LLM that...\n", - " return [\"The answer to your question lies within.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -176,11 +132,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -204,11 +156,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "model = ChatAnthropic(model=\"claude-3-haiku-20240307\")" - ] + "source": ["from langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-haiku-20240307\")"] }, { "cell_type": "markdown", @@ -226,9 +174,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -267,29 +213,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state: State) -> Literal[\"end\", \"continue\"]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no tool call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "async def call_model(state: State):\n", - " messages = state[\"messages\"]\n", - " response = await model.ainvoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"end\", \"continue\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no tool call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\nasync def call_model(state: State):\n messages = state[\"messages\"]\n response = await model.ainvoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "markdown", @@ -307,50 +231,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -369,11 +250,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -406,12 +283,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "await app.ainvoke(inputs)" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nawait app.ainvoke(inputs)"] }, { "cell_type": "markdown", @@ -480,16 +352,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "async for output in app.astream(inputs, stream_mode=\"updates\"):\n", - " # stream_mode=\"updates\" yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value[\"messages\"][-1].pretty_print())\n", - " print(\"\\n---\\n\")" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nasync for output in app.astream(inputs, stream_mode=\"updates\"):\n # stream_mode=\"updates\" yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value[\"messages\"][-1].pretty_print())\n print(\"\\n---\\n\")"] }, { "cell_type": "markdown", @@ -546,20 +409,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", - " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", - " for op in output.ops:\n", - " if op[\"path\"] == \"/streamed_output/-\":\n", - " # this is the output from .stream()\n", - " ...\n", - " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", - " \"/streamed_output/-\"\n", - " ):\n", - " # because we chose to only include LLMs, these are LLM tokens\n", - " print(op[\"value\"].content, end=\"|\")" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nasync for output in app.astream_log(inputs, include_types=[\"llm\"]):\n # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n for op in output.ops:\n if op[\"path\"] == \"/streamed_output/-\":\n # this is the output from .stream()\n ...\n elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n \"/streamed_output/-\"\n ):\n # because we chose to only include LLMs, these are LLM tokens\n print(op[\"value\"].content, end=\"|\")"] }, { "cell_type": "code", @@ -567,7 +417,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/branching.ipynb b/examples/branching.ipynb index f0fbe01c6..cf0024cdd 100644 --- a/examples/branching.ipynb +++ b/examples/branching.ipynb @@ -20,10 +20,7 @@ "id": "bb54e2d0", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph"] }, { "cell_type": "markdown", @@ -39,42 +36,7 @@ "id": "09372b8b-edea-4b9d-9ec3-3d93ce1ba819", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Any\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # The operator.add reducer fn makes this append-only\n", - " aggregate: Annotated[list, operator.add]\n", - "\n", - "\n", - "class ReturnNodeValue:\n", - " def __init__(self, node_secret: str):\n", - " self._value = node_secret\n", - "\n", - " def __call__(self, state: State) -> Any:\n", - " print(f\"Adding {self._value} to {state['aggregate']}\")\n", - " return {\"aggregate\": [self._value]}\n", - "\n", - "\n", - "builder = StateGraph(State)\n", - "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n", - "builder.set_entry_point(\"a\")\n", - "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n", - "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n", - "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n", - "builder.add_edge(\"a\", \"b\")\n", - "builder.add_edge(\"a\", \"c\")\n", - "builder.add_edge(\"b\", \"d\")\n", - "builder.add_edge(\"c\", \"d\")\n", - "builder.set_finish_point(\"d\")\n", - "graph = builder.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, Any\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n\n\nclass ReturnNodeValue:\n def __init__(self, node_secret: str):\n self._value = node_secret\n\n def __call__(self, state: State) -> Any:\n print(f\"Adding {self._value} to {state['aggregate']}\")\n return {\"aggregate\": [self._value]}\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_edge(\"a\", \"b\")\nbuilder.add_edge(\"a\", \"c\")\nbuilder.add_edge(\"b\", \"d\")\nbuilder.add_edge(\"c\", \"d\")\nbuilder.set_finish_point(\"d\")\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -93,11 +55,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(graph.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"] }, { "cell_type": "code", @@ -126,9 +84,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})" - ] + "source": ["graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})"] }, { "cell_type": "markdown", @@ -162,34 +118,7 @@ "id": "259a7704-5aa0-4e4c-aeef-cca04e8be0ff", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # The operator.add reducer fn makes this append-only\n", - " aggregate: Annotated[list, operator.add]\n", - "\n", - "\n", - "builder = StateGraph(State)\n", - "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n", - "builder.set_entry_point(\"a\")\n", - "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n", - "builder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\n", - "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n", - "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n", - "builder.add_edge(\"a\", \"b\")\n", - "builder.add_edge(\"a\", \"c\")\n", - "builder.add_edge(\"b\", \"b2\")\n", - "builder.add_edge([\"b2\", \"c\"], \"d\")\n", - "builder.set_finish_point(\"d\")\n", - "graph = builder.compile()" - ] + "source": ["import operator\nfrom typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_edge(\"a\", \"b\")\nbuilder.add_edge(\"a\", \"c\")\nbuilder.add_edge(\"b\", \"b2\")\nbuilder.add_edge([\"b2\", \"c\"], \"d\")\nbuilder.set_finish_point(\"d\")\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -208,11 +137,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(graph.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"] }, { "cell_type": "code", @@ -242,9 +167,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"aggregate\": []})" - ] + "source": ["graph.invoke({\"aggregate\": []})"] }, { "cell_type": "markdown", @@ -264,49 +187,7 @@ "id": "95f5e026", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import END, START, StateGraph\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # The operator.add reducer fn makes this append-only\n", - " aggregate: Annotated[list, operator.add]\n", - " which: str\n", - "\n", - "\n", - "builder = StateGraph(State)\n", - "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n", - "builder.add_edge(START, \"a\")\n", - "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n", - "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n", - "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n", - "builder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n", - "\n", - "\n", - "def route_bc_or_cd(state: State) -> Sequence[str]:\n", - " if state[\"which\"] == \"cd\":\n", - " return [\"c\", \"d\"]\n", - " return [\"b\", \"c\"]\n", - "\n", - "\n", - "intermediates = [\"b\", \"c\", \"d\"]\n", - "builder.add_conditional_edges(\n", - " \"a\",\n", - " route_bc_or_cd,\n", - " intermediates,\n", - ")\n", - "for node in intermediates:\n", - " builder.add_edge(node, \"e\")\n", - "\n", - "\n", - "builder.add_edge(\"e\", END)\n", - "graph = builder.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, START, StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n which: str\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n\n\ndef route_bc_or_cd(state: State) -> Sequence[str]:\n if state[\"which\"] == \"cd\":\n return [\"c\", \"d\"]\n return [\"b\", \"c\"]\n\n\nintermediates = [\"b\", \"c\", \"d\"]\nbuilder.add_conditional_edges(\n \"a\",\n route_bc_or_cd,\n intermediates,\n)\nfor node in intermediates:\n builder.add_edge(node, \"e\")\n\n\nbuilder.add_edge(\"e\", END)\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -325,11 +206,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(graph.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"] }, { "cell_type": "code", @@ -358,9 +235,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"aggregate\": [], \"which\": \"bc\"})" - ] + "source": ["graph.invoke({\"aggregate\": [], \"which\": \"bc\"})"] }, { "cell_type": "code", @@ -389,9 +264,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"aggregate\": [], \"which\": \"cd\"})" - ] + "source": ["graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"] }, { "cell_type": "markdown", @@ -413,92 +286,7 @@ "id": "836bc12d", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "\n", - "\n", - "def reduce_fanouts(left, right):\n", - " if left is None:\n", - " left = []\n", - " if not right:\n", - " # Overwrite\n", - " return []\n", - " return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # The operator.add reducer fn makes this append-only\n", - " aggregate: Annotated[list, operator.add]\n", - " fanout_values: Annotated[list, reduce_fanouts]\n", - " which: str\n", - "\n", - "\n", - "builder = StateGraph(State)\n", - "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n", - "builder.set_entry_point(\"a\")\n", - "\n", - "\n", - "class ParallelReturnNodeValue:\n", - " def __init__(\n", - " self,\n", - " node_secret: str,\n", - " reliability: float,\n", - " ):\n", - " self._value = node_secret\n", - " self._reliability = reliability\n", - "\n", - " def __call__(self, state: State) -> Any:\n", - " print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n", - " return {\n", - " \"fanout_values\": [\n", - " {\n", - " \"value\": [self._value],\n", - " \"reliability\": self._reliability,\n", - " }\n", - " ]\n", - " }\n", - "\n", - "\n", - "builder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.9))\n", - "\n", - "builder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.1))\n", - "builder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.3))\n", - "\n", - "\n", - "def aggregate_fanout_values(state: State) -> Any:\n", - " # Sort by reliability\n", - " ranked_values = sorted(\n", - " state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n", - " )\n", - " return {\n", - " \"aggregate\": [x[\"value\"] for x in ranked_values] + [\"I'm E\"],\n", - " \"fanout_values\": [],\n", - " }\n", - "\n", - "\n", - "builder.add_node(\"e\", aggregate_fanout_values)\n", - "\n", - "\n", - "def route_bc_or_cd(state: State) -> Sequence[str]:\n", - " if state[\"which\"] == \"cd\":\n", - " return [\"c\", \"d\"]\n", - " return [\"b\", \"c\"]\n", - "\n", - "\n", - "intermediates = [\"b\", \"c\", \"d\"]\n", - "builder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n", - "\n", - "for node in intermediates:\n", - " builder.add_edge(node, \"e\")\n", - "\n", - "builder.set_finish_point(\"e\")\n", - "graph = builder.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\ndef reduce_fanouts(left, right):\n if left is None:\n left = []\n if not right:\n # Overwrite\n return []\n return left + right\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n fanout_values: Annotated[list, reduce_fanouts]\n which: str\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\n\n\nclass ParallelReturnNodeValue:\n def __init__(\n self,\n node_secret: str,\n reliability: float,\n ):\n self._value = node_secret\n self._reliability = reliability\n\n def __call__(self, state: State) -> Any:\n print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n return {\n \"fanout_values\": [\n {\n \"value\": [self._value],\n \"reliability\": self._reliability,\n }\n ]\n }\n\n\nbuilder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.9))\n\nbuilder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.1))\nbuilder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.3))\n\n\ndef aggregate_fanout_values(state: State) -> Any:\n # Sort by reliability\n ranked_values = sorted(\n state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n )\n return {\n \"aggregate\": [x[\"value\"] for x in ranked_values] + [\"I'm E\"],\n \"fanout_values\": [],\n }\n\n\nbuilder.add_node(\"e\", aggregate_fanout_values)\n\n\ndef route_bc_or_cd(state: State) -> Sequence[str]:\n if state[\"which\"] == \"cd\":\n return [\"c\", \"d\"]\n return [\"b\", \"c\"]\n\n\nintermediates = [\"b\", \"c\", \"d\"]\nbuilder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n\nfor node in intermediates:\n builder.add_edge(node, \"e\")\n\nbuilder.set_finish_point(\"e\")\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -517,11 +305,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(graph.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"] }, { "cell_type": "code", @@ -551,9 +335,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})" - ] + "source": ["graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})"] }, { "cell_type": "code", @@ -583,9 +365,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"aggregate\": [], \"which\": \"cd\"})" - ] + "source": ["graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/anthropic.ipynb b/examples/chat_agent_executor_with_function_calling/anthropic.ipynb index 3c7f975b7..26327dda8 100644 --- a/examples/chat_agent_executor_with_function_calling/anthropic.ipynb +++ b/examples/chat_agent_executor_with_function_calling/anthropic.ipynb @@ -27,10 +27,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langchain langchain_anthropic tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langchain langchain_anthropic tavily-python"] }, { "cell_type": "markdown", @@ -46,13 +43,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -68,10 +59,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -95,11 +83,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -123,11 +107,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "model = ChatAnthropic(temperature=0, model_name=\"claude-3-opus-20240229\")" - ] + "source": ["from langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(temperature=0, model_name=\"claude-3-opus-20240229\")"] }, { "cell_type": "markdown", @@ -154,9 +134,7 @@ ] } ], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "code", @@ -164,16 +142,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -208,33 +177,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there are no tool calls, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there are no tool calls, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -252,50 +195,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "markdown", @@ -328,12 +228,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "app.invoke(inputs)" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\napp.invoke(inputs)"] }, { "cell_type": "markdown", @@ -383,16 +278,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/base.ipynb b/examples/chat_agent_executor_with_function_calling/base.ipynb index f9384f68f..5b3744db5 100644 --- a/examples/chat_agent_executor_with_function_calling/base.ipynb +++ b/examples/chat_agent_executor_with_function_calling/base.ipynb @@ -26,10 +26,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -45,13 +42,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -67,10 +58,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -90,11 +78,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -112,11 +96,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -140,13 +120,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -164,9 +138,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -192,16 +164,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -236,53 +199,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " tool_call = last_message.tool_calls[0]\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a FunctionMessage\n", - " function_message = ToolMessage(\n", - " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", - " )\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [function_message]}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a FunctionMessage\n function_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [function_message]}"] }, { "cell_type": "markdown", @@ -300,50 +217,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -362,15 +236,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -403,12 +269,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "app.invoke(inputs)" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\napp.invoke(inputs)"] }, { "cell_type": "markdown", @@ -458,16 +319,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "markdown", @@ -604,21 +456,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf?\")]}\n", - "\n", - "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", - " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", - " for op in output.ops:\n", - " if op[\"path\"] == \"/streamed_output/-\":\n", - " # this is the output from .stream()\n", - " ...\n", - " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", - " \"/streamed_output/-\"\n", - " ):\n", - " # because we chose to only include LLMs, these are LLM tokens\n", - " print(op[\"value\"])" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf?\")]}\n\nasync for output in app.astream_log(inputs, include_types=[\"llm\"]):\n # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n for op in output.ops:\n if op[\"path\"] == \"/streamed_output/-\":\n # this is the output from .stream()\n ...\n elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n \"/streamed_output/-\"\n ):\n # because we chose to only include LLMs, these are LLM tokens\n print(op[\"value\"])"] }, { "cell_type": "code", @@ -626,7 +464,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb b/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb index 8ac2e500b..9d3fc69bf 100644 --- a/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb +++ b/examples/chat_agent_executor_with_function_calling/dynamically-returning-directly.ipynb @@ -38,10 +38,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -57,13 +54,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -79,10 +70,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -106,19 +94,7 @@ "id": "4a1b9990-3b11-4a51-bd51-76117afd38b9", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class SearchTool(BaseModel):\n", - " \"\"\"Look up things online, optionally returning directly\"\"\"\n", - "\n", - " query: str = Field(description=\"query to look up online\")\n", - " return_direct: bool = Field(\n", - " description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\",\n", - " default=False,\n", - " )" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass SearchTool(BaseModel):\n \"\"\"Look up things online, optionally returning directly\"\"\"\n\n query: str = Field(description=\"query to look up online\")\n return_direct: bool = Field(\n description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\",\n default=False,\n )"] }, { "cell_type": "code", @@ -126,12 +102,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\n", - "tools = [search_tool]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\nsearch_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\ntools = [search_tool]"] }, { "cell_type": "markdown", @@ -149,11 +120,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -177,13 +144,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -201,9 +162,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -229,16 +188,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -273,11 +223,7 @@ "id": "03308b6b-de72-4cdc-b6c6-47e654df340e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation"] }, { "cell_type": "markdown", @@ -295,22 +241,7 @@ "id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we check if it's suppose to return direct\n", - " else:\n", - " arguments = last_message.tool_calls[0][\"args\"]\n", - " if arguments.get(\"return_direct\", False):\n", - " return \"final\"\n", - " else:\n", - " return \"continue\"" - ] + "source": ["# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we check if it's suppose to return direct\n else:\n arguments = last_message.tool_calls[0][\"args\"]\n if arguments.get(\"return_direct\", False):\n return \"final\"\n else:\n return \"continue\""] }, { "cell_type": "code", @@ -318,14 +249,7 @@ "id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "markdown", @@ -343,33 +267,7 @@ "id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991", "metadata": {}, "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " tool_call = last_message.tool_calls[0]\n", - " tool_name = tool_call[\"name\"]\n", - " arguments = tool_call[\"args\"]\n", - " if tool_name == \"tavily_search_results_json\":\n", - " if \"return_direct\" in arguments:\n", - " del arguments[\"return_direct\"]\n", - " action = ToolInvocation(\n", - " tool=tool_name,\n", - " tool_input=arguments,\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a ToolMessage\n", - " tool_message = ToolMessage(\n", - " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", - " )\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n tool_name = tool_call[\"name\"]\n arguments = tool_call[\"args\"]\n if tool_name == \"tavily_search_results_json\":\n if \"return_direct\" in arguments:\n del arguments[\"return_direct\"]\n action = ToolInvocation(\n tool=tool_name,\n tool_input=arguments,\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -391,54 +289,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "workflow.add_node(\"final\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Final call\n", - " \"final\": \"final\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "workflow.add_edge(\"final\", END)\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\nworkflow.add_node(\"final\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Final call\n \"final\": \"final\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\nworkflow.add_edge(\"final\", END)\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -457,15 +308,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -509,18 +352,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -547,24 +379,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"what is the weather in sf? return this result directly by setting return_direct = True\"\n", - " )\n", - " ]\n", - "}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\n \"messages\": [\n HumanMessage(\n content=\"what is the weather in sf? return this result directly by setting return_direct = True\"\n )\n ]\n}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -572,7 +387,7 @@ "id": "49ccc134-4abe-4982-8ecd-d70fc56a4d2d", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb b/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb index b2ae9ba1a..f13ed184f 100644 --- a/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb +++ b/examples/chat_agent_executor_with_function_calling/force-calling-a-tool-first.ipynb @@ -30,10 +30,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -49,18 +46,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -76,10 +62,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -99,19 +82,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder, but don't tell the LLM that...\n", - " return [\"The answer to your question lies within.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -129,11 +100,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -157,11 +124,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -179,9 +142,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -207,16 +168,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -251,69 +203,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state: AgentState):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state: AgentState):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "# We recommend you use ToolNode\n", - "# for this, but we are showing the\n", - "# manual way here for clarity\n", - "def call_tool(state: AgentState):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation for each tool call\n", - " tool_invocations = []\n", - " for tool_call in last_message.tool_calls:\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " tool_invocations.append(action)\n", - "\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n", - " # We use the response to create tool messages\n", - " tool_messages = [\n", - " ToolMessage(\n", - " content=str(response),\n", - " name=tc[\"name\"],\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc, response in zip(last_message.tool_calls, responses)\n", - " ]\n", - "\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: AgentState):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state: AgentState):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\n# We recommend you use ToolNode\n# for this, but we are showing the\n# manual way here for clarity\ndef call_tool(state: AgentState):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] }, { "cell_type": "markdown", @@ -331,30 +221,7 @@ "id": "1bfd2b22-292a-4f4d-91a0-46bb704f5e38", "metadata": {}, "outputs": [], - "source": [ - "# This is the new first - the first call of the model we want to explicitly hard-code some action\n", - "from langchain_core.messages import AIMessage\n", - "\n", - "\n", - "def first_model(state: AgentState):\n", - " human_input = state[\"messages\"][-1].content\n", - " return {\n", - " \"messages\": [\n", - " AIMessage(\n", - " content=\"\",\n", - " tool_calls=[\n", - " {\n", - " \"name\": \"tavily_search_results_json\",\n", - " \"args\": {\n", - " \"query\": human_input,\n", - " },\n", - " \"id\": \"tool_abcd123\",\n", - " }\n", - " ],\n", - " )\n", - " ]\n", - " }" - ] + "source": ["# This is the new first - the first call of the model we want to explicitly hard-code some action\nfrom langchain_core.messages import AIMessage\n\n\ndef first_model(state: AgentState):\n human_input = state[\"messages\"][-1].content\n return {\n \"messages\": [\n AIMessage(\n content=\"\",\n tool_calls=[\n {\n \"name\": \"tavily_search_results_json\",\n \"args\": {\n \"query\": human_input,\n },\n \"id\": \"tool_abcd123\",\n }\n ],\n )\n ]\n }"] }, { "cell_type": "markdown", @@ -376,56 +243,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the new entrypoint\n", - "workflow.add_node(\"first_agent\", first_model)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"first_agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# After we call the first agent, we know we want to go to action\n", - "workflow.add_edge(\"first_agent\", \"action\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the new entrypoint\nworkflow.add_node(\"first_agent\", first_model)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"first_agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After we call the first agent, we know we want to go to action\nworkflow.add_edge(\"first_agent\", \"action\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -444,11 +262,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -670,17 +484,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs, stream_mode=\"values\"):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " messages = output[\"messages\"]\n", - " for message in messages:\n", - " message.pretty_print()\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs, stream_mode=\"values\"):\n # stream() yields dictionaries with output keyed by node name\n messages = output[\"messages\"]\n for message in messages:\n message.pretty_print()\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -688,7 +492,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb b/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb index 9ab467887..867afabaf 100644 --- a/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb +++ b/examples/chat_agent_executor_with_function_calling/human-in-the-loop.ipynb @@ -30,10 +30,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_community langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_community langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -58,13 +55,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -88,10 +79,7 @@ ] } ], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -111,11 +99,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -133,11 +117,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -161,13 +141,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -185,9 +159,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -213,16 +185,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -257,31 +220,7 @@ "id": "b547109f-f9e8-4e77-a7e7-ed2bae7a72ab", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "code", @@ -289,41 +228,7 @@ "id": "73fd6432-42e8-472a-89ca-bb5ddbbcc35a", "metadata": {}, "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation for each tool call\n", - " tool_invocations = []\n", - " for tool_call in last_message.tool_calls:\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " tool_invocations.append(action)\n", - "\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n", - " # We use the response to create tool messages\n", - " tool_messages = [\n", - " ToolMessage(\n", - " content=str(response),\n", - " name=tc[\"name\"],\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc, response in zip(last_message.tool_calls, responses)\n", - " ]\n", - "\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" - ] + "source": ["# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] }, { "cell_type": "markdown", @@ -345,51 +250,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile(checkpointer=MemorySaver(), interrupt_before=[\"action\"])" - ] + "source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=MemorySaver(), interrupt_before=[\"action\"])"] }, { "cell_type": "code", @@ -408,15 +269,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -460,30 +313,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "config = {\"configurable\": {\"thread_id\": \"thread-1\"}}\n", - "while True:\n", - " for output in app.stream(inputs, config):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")\n", - " snapshot = app.get_state(config)\n", - " # If \"next\" is present, it means we've interrupted mid-execution\n", - " if not snapshot.next:\n", - " break\n", - " inputs = None\n", - " response = input(\n", - " \"Do you approve the next step? Type y if you do, anything else to stop: \"\n", - " )\n", - " if response != \"y\":\n", - " break" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nconfig = {\"configurable\": {\"thread_id\": \"thread-1\"}}\nwhile True:\n for output in app.stream(inputs, config):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n snapshot = app.get_state(config)\n # If \"next\" is present, it means we've interrupted mid-execution\n if not snapshot.next:\n break\n inputs = None\n response = input(\n \"Do you approve the next step? Type y if you do, anything else to stop: \"\n )\n if response != \"y\":\n break"] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb b/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb index 35f494b95..862563ddf 100644 --- a/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb +++ b/examples/chat_agent_executor_with_function_calling/managing-agent-steps.ipynb @@ -30,10 +30,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -49,13 +46,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -71,10 +62,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -94,11 +82,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -116,11 +100,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -144,13 +124,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -168,9 +142,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -196,16 +168,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -240,23 +203,7 @@ "id": "e718a9c5-6596-457f-ac25-a25d8cb8c259", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -274,14 +221,7 @@ "id": "714e4135-7cb5-4f17-b2ae-46f7e98bde61", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"][-5:]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"][-5:]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "code", @@ -289,41 +229,7 @@ "id": "b3ca9564-63cc-4309-b158-5e8d3e907164", "metadata": {}, "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation for each tool call\n", - " tool_invocations = []\n", - " for tool_call in last_message.tool_calls:\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " tool_invocations.append(action)\n", - "\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n", - " # We use the response to create tool messages\n", - " tool_messages = [\n", - " ToolMessage(\n", - " content=str(response),\n", - " name=tc[\"name\"],\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc, response in zip(last_message.tool_calls, responses)\n", - " ]\n", - "\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" - ] + "source": ["# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] }, { "cell_type": "markdown", @@ -341,50 +247,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -403,15 +266,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -455,18 +310,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -474,7 +318,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/prebuilt-tool-node.ipynb b/examples/chat_agent_executor_with_function_calling/prebuilt-tool-node.ipynb index da0e8b3c9..fe8231e73 100644 --- a/examples/chat_agent_executor_with_function_calling/prebuilt-tool-node.ipynb +++ b/examples/chat_agent_executor_with_function_calling/prebuilt-tool-node.ipynb @@ -27,10 +27,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -46,13 +43,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -68,10 +59,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -95,11 +83,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -123,11 +107,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -145,9 +125,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -173,16 +151,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -217,33 +186,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there are no tool calls, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there are no tool calls, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -261,50 +204,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "markdown", @@ -337,12 +237,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "app.invoke(inputs)" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\napp.invoke(inputs)"] }, { "cell_type": "markdown", @@ -392,16 +287,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "markdown", @@ -496,21 +382,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf?\")]}\n", - "\n", - "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", - " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", - " for op in output.ops:\n", - " if op[\"path\"] == \"/streamed_output/-\":\n", - " # this is the output from .stream()\n", - " ...\n", - " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", - " \"/streamed_output/-\"\n", - " ):\n", - " # because we chose to only include LLMs, these are LLM tokens\n", - " print(op[\"value\"])" - ] + "source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf?\")]}\n\nasync for output in app.astream_log(inputs, include_types=[\"llm\"]):\n # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n for op in output.ops:\n if op[\"path\"] == \"/streamed_output/-\":\n # this is the output from .stream()\n ...\n elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n \"/streamed_output/-\"\n ):\n # because we chose to only include LLMs, these are LLM tokens\n print(op[\"value\"])"] }, { "cell_type": "code", @@ -518,7 +390,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb b/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb index 3e1f82577..1cd5809ad 100644 --- a/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb +++ b/examples/chat_agent_executor_with_function_calling/respond-in-format.ipynb @@ -38,22 +38,7 @@ "id": "de1db3c1", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# Add messages essentially does this with more\n", - "# robust handling\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -73,19 +58,7 @@ "id": "23a2ca43", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder, but don't tell the LLM that...\n", - " return [\"The answer to your question lies within.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -102,11 +75,7 @@ "id": "979512e4", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -130,11 +99,7 @@ "id": "1c8132c5", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "model = ChatAnthropic(model=\"claude-3-haiku-20240307\")" - ] + "source": ["from langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-haiku-20240307\")"] }, { "cell_type": "markdown", @@ -152,9 +117,7 @@ "id": "055d84bf", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -172,10 +135,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -191,13 +151,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -213,10 +167,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -236,11 +187,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -258,11 +205,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -286,13 +229,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -315,19 +252,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class Response(BaseModel):\n", - " \"\"\"Final response to the user\"\"\"\n", - "\n", - " temperature: float = Field(description=\"the temperature\")\n", - " other_notes: str = Field(description=\"any other notes about the weather\")\n", - "\n", - "\n", - "model = model.bind_tools(tools + [Response])" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Response(BaseModel):\n \"\"\"Final response to the user\"\"\"\n\n temperature: float = Field(description=\"the temperature\")\n other_notes: str = Field(description=\"any other notes about the weather\")\n\n\nmodel = model.bind_tools(tools + [Response])"] }, { "cell_type": "markdown", @@ -353,16 +278,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -401,70 +317,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state) -> Literal[\"continue\", \"end\"]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we need to check what type of function call it is\n", - " if last_message.tool_calls[0][\"name\"] == \"Response\":\n", - " return \"end\"\n", - " # Otherwise we continue\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation for each tool call\n", - " tool_invocations = []\n", - " for tool_call in last_message.tool_calls:\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " tool_invocations.append(action)\n", - "\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n", - " # We use the response to create tool messages\n", - " tool_messages = [\n", - " ToolMessage(\n", - " content=str(response),\n", - " name=tc[\"name\"],\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc, response in zip(last_message.tool_calls, responses)\n", - " ]\n", - "\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" - ] + "source": ["from typing import Literal\n\nfrom langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state) -> Literal[\"continue\", \"end\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we need to check what type of function call it is\n if last_message.tool_calls[0][\"name\"] == \"Response\":\n return \"end\"\n # Otherwise we continue\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] }, { "cell_type": "markdown", @@ -482,50 +335,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -544,15 +354,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -596,18 +398,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value[\"messages\"][-1])\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value[\"messages\"][-1])\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -615,7 +406,7 @@ "id": "eed4360d-2cdf-497b-b03f-8bc51062f780", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index 044572c7b..98c130a52 100644 --- a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -26,10 +26,7 @@ "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", @@ -37,24 +34,7 @@ "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", "metadata": {}, "outputs": [], - "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\"" - ] + "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\""] }, { "cell_type": "markdown", @@ -75,24 +55,7 @@ "id": "828479af-cf9c-4888-a365-599643a96b55", "metadata": {}, "outputs": [], - "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()" - ] + "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()"] }, { "cell_type": "code", @@ -114,9 +77,7 @@ "output_type": "execute_result" } ], - "source": [ - "my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])" - ] + "source": ["my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"] }, { "cell_type": "markdown", @@ -135,33 +96,7 @@ "id": "32c147df-7f90-4b0d-9a6b-671677020353", "metadata": {}, "outputs": [], - "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" - ] + "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"] }, { "cell_type": "code", @@ -180,12 +115,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "messages = [HumanMessage(content=\"Hi! How can I help you?\")]\n", - "simulated_user.invoke({\"messages\": messages})" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nmessages = [HumanMessage(content=\"Hi! How can I help you?\")]\nsimulated_user.invoke({\"messages\": messages})"] }, { "cell_type": "markdown", @@ -223,19 +153,7 @@ "id": "69e2a3a3-40f3-4223-9136-113738440be9", "metadata": {}, "outputs": [], - "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(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\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\"])"] }, { "cell_type": "markdown", @@ -251,25 +169,7 @@ "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", - "\n", - "def 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\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)"] }, { "cell_type": "markdown", @@ -292,15 +192,7 @@ "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(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", @@ -318,29 +210,7 @@ "id": "0b597e4b-4cbb-4bbc-82e5-f7e31275964c", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, MessageGraph\n", - "\n", - "graph_builder = MessageGraph()\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.set_entry_point(\"chat_bot\")\n", - "simulation = graph_builder.compile()" - ] + "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()"] }, { "cell_type": "markdown", @@ -381,13 +251,7 @@ ] } ], - "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", @@ -395,7 +259,7 @@ "id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/chatbot-simulation-evaluation/simulation_utils.py b/examples/chatbot-simulation-evaluation/simulation_utils.py index 998a8330e..be3b32e8c 100644 --- a/examples/chatbot-simulation-evaluation/simulation_utils.py +++ b/examples/chatbot-simulation-evaluation/simulation_utils.py @@ -9,7 +9,7 @@ from langchain_core.runnables import chain as as_runnable from langchain_openai import ChatOpenAI from typing_extensions import TypedDict -from langgraph.graph import END, StateGraph +from langgraph.graph import END, StateGraph, START def langchain_to_openai_messages(messages: List[BaseMessage]): @@ -116,7 +116,7 @@ def create_chat_simulator( should_continue or functools.partial(_should_continue, max_turns=max_turns), ) # If your dataset has a 'leading question/input', then we route first to the assistant, otherwise, we let the user take the lead. - graph_builder.set_entry_point("assistant" if input_key is not None else "user") + graph_builder.add_edge(START, "assistant" if input_key is not None else "user") return ( RunnableLambda(_prepare_example).bind(input_key=input_key) diff --git a/examples/code_assistant/langgraph_code_assistant.ipynb b/examples/code_assistant/langgraph_code_assistant.ipynb index 02a9759f6..2115a370f 100644 --- a/examples/code_assistant/langgraph_code_assistant.ipynb +++ b/examples/code_assistant/langgraph_code_assistant.ipynb @@ -34,9 +34,7 @@ "id": "e3900420", "metadata": {}, "outputs": [], - "source": [ - "! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4" - ] + "source": ["! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4"] }, { "cell_type": "markdown", @@ -54,24 +52,7 @@ "id": "c2eb35d1-4990-47dc-a5c4-208bae588a82", "metadata": {}, "outputs": [], - "source": [ - "from bs4 import BeautifulSoup as Soup\n", - "from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n", - "\n", - "# LCEL docs\n", - "url = \"https://python.langchain.com/v0.2/docs/concepts/#langchain-expression-language-lcel\"\n", - "loader = RecursiveUrlLoader(\n", - " url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n", - ")\n", - "docs = loader.load()\n", - "\n", - "# Sort the list based on the URLs and get the text\n", - "d_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\n", - "d_reversed = list(reversed(d_sorted))\n", - "concatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n", - " [doc.page_content for doc in d_reversed]\n", - ")" - ] + "source": ["from bs4 import BeautifulSoup as Soup\nfrom langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n\n# LCEL docs\nurl = \"https://python.langchain.com/v0.2/docs/concepts/#langchain-expression-language-lcel\"\nloader = RecursiveUrlLoader(\n url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n)\ndocs = loader.load()\n\n# Sort the list based on the URLs and get the text\nd_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\nd_reversed = list(reversed(d_sorted))\nconcatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n [doc.page_content for doc in d_reversed]\n)"] }, { "cell_type": "markdown", @@ -93,45 +74,7 @@ "id": "3ba3df70-f6b4-4ea5-a210-e10944960bc6", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "### OpenAI\n", - "\n", - "# Grader prompt\n", - "code_gen_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n", - " Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n", - " question based on the above provided documentation. Ensure any code you provide can be executed \\n \n", - " with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n", - " Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ")\n", - "\n", - "\n", - "# Data model\n", - "class code(BaseModel):\n", - " \"\"\"Code output\"\"\"\n", - "\n", - " prefix: str = Field(description=\"Description of the problem and approach\")\n", - " imports: str = Field(description=\"Code block import statements\")\n", - " code: str = Field(description=\"Code block not including import statements\")\n", - " description = \"Schema for code solutions to questions about LCEL.\"\n", - "\n", - "\n", - "expt_llm = \"gpt-4-0125-preview\"\n", - "llm = ChatOpenAI(temperature=0, model=expt_llm)\n", - "code_gen_chain = code_gen_prompt | llm.with_structured_output(code)\n", - "question = \"How do I build a RAG chain in LCEL?\"\n", - "# solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})" - ] + "source": ["from langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n### OpenAI\n\n# Grader prompt\ncode_gen_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n question based on the above provided documentation. Ensure any code you provide can be executed \\n \n with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\nexpt_llm = \"gpt-4-0125-preview\"\nllm = ChatOpenAI(temperature=0, model=expt_llm)\ncode_gen_chain = code_gen_prompt | llm.with_structured_output(code)\nquestion = \"How do I build a RAG chain in LCEL?\"\n# solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})"] }, { "cell_type": "code", @@ -139,118 +82,7 @@ "id": "cd30b67d-96db-4e51-a540-ae23fcc1f878", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "### Anthropic\n", - "\n", - "# Prompt to enforce tool use\n", - "code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\" You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n", - " Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n", - " above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n", - " defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n", - " Invoke the code tool to structure the output correctly. \\n Here is the user question:\"\"\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ")\n", - "\n", - "\n", - "# Data model\n", - "class code(BaseModel):\n", - " \"\"\"Code output\"\"\"\n", - "\n", - " prefix: str = Field(description=\"Description of the problem and approach\")\n", - " imports: str = Field(description=\"Code block import statements\")\n", - " code: str = Field(description=\"Code block not including import statements\")\n", - " description = \"Schema for code solutions to questions about LCEL.\"\n", - "\n", - "\n", - "# LLM\n", - "# expt_llm = \"claude-3-haiku-20240307\"\n", - "expt_llm = \"claude-3-opus-20240229\"\n", - "llm = ChatAnthropic(\n", - " model=expt_llm,\n", - " default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n", - ")\n", - "\n", - "structured_llm_claude = llm.with_structured_output(code, include_raw=True)\n", - "\n", - "\n", - "# Optional: Check for errors in case tool use is flaky\n", - "def check_claude_output(tool_output):\n", - " \"\"\"Check for parse error or failure to call the tool\"\"\"\n", - "\n", - " # Error with parsing\n", - " if tool_output[\"parsing_error\"]:\n", - " # Report back output and parsing errors\n", - " print(\"Parsing error!\")\n", - " raw_output = str(tool_output[\"raw\"].content)\n", - " error = tool_output[\"parsing_error\"]\n", - " raise ValueError(\n", - " f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n", - " )\n", - "\n", - " # Tool was not invoked\n", - " elif not tool_output[\"parsed\"]:\n", - " print(\"Failed to invoke tool!\")\n", - " raise ValueError(\n", - " \"You did not use the provided tool! Be sure to invoke the tool to structure the output.\"\n", - " )\n", - " return tool_output\n", - "\n", - "\n", - "# Chain with output check\n", - "code_chain_claude_raw = (\n", - " code_gen_prompt_claude | structured_llm_claude | check_claude_output\n", - ")\n", - "\n", - "\n", - "def insert_errors(inputs):\n", - " \"\"\"Insert errors for tool parsing in the messages\"\"\"\n", - "\n", - " # Get errors\n", - " error = inputs[\"error\"]\n", - " messages = inputs[\"messages\"]\n", - " messages += [\n", - " (\n", - " \"assistant\",\n", - " f\"Retry. You are required to fix the parsing errors: {error} \\n\\n You must invoke the provided tool.\",\n", - " )\n", - " ]\n", - " return {\n", - " \"messages\": messages,\n", - " \"context\": inputs[\"context\"],\n", - " }\n", - "\n", - "\n", - "# This will be run as a fallback chain\n", - "fallback_chain = insert_errors | code_chain_claude_raw\n", - "N = 3 # Max re-tries\n", - "code_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n", - " fallbacks=[fallback_chain] * N, exception_key=\"error\"\n", - ")\n", - "\n", - "\n", - "def parse_output(solution):\n", - " \"\"\"When we add 'include_raw=True' to structured output,\n", - " it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n", - "\n", - " return solution[\"parsed\"]\n", - "\n", - "\n", - "# Optional: With re-try to correct for failure to invoke tool\n", - "code_gen_chain = code_gen_chain_re_try | parse_output\n", - "\n", - "# No re-try\n", - "code_gen_chain = code_gen_prompt_claude | structured_llm_claude | parse_output" - ] + "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n### Anthropic\n\n# Prompt to enforce tool use\ncode_gen_prompt_claude = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\" You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n Invoke the code tool to structure the output correctly. \\n Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\n# LLM\n# expt_llm = \"claude-3-haiku-20240307\"\nexpt_llm = \"claude-3-opus-20240229\"\nllm = ChatAnthropic(\n model=expt_llm,\n default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n)\n\nstructured_llm_claude = llm.with_structured_output(code, include_raw=True)\n\n\n# Optional: Check for errors in case tool use is flaky\ndef check_claude_output(tool_output):\n \"\"\"Check for parse error or failure to call the tool\"\"\"\n\n # Error with parsing\n if tool_output[\"parsing_error\"]:\n # Report back output and parsing errors\n print(\"Parsing error!\")\n raw_output = str(tool_output[\"raw\"].content)\n error = tool_output[\"parsing_error\"]\n raise ValueError(\n f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n )\n\n # Tool was not invoked\n elif not tool_output[\"parsed\"]:\n print(\"Failed to invoke tool!\")\n raise ValueError(\n \"You did not use the provided tool! Be sure to invoke the tool to structure the output.\"\n )\n return tool_output\n\n\n# Chain with output check\ncode_chain_claude_raw = (\n code_gen_prompt_claude | structured_llm_claude | check_claude_output\n)\n\n\ndef insert_errors(inputs):\n \"\"\"Insert errors for tool parsing in the messages\"\"\"\n\n # Get errors\n error = inputs[\"error\"]\n messages = inputs[\"messages\"]\n messages += [\n (\n \"assistant\",\n f\"Retry. You are required to fix the parsing errors: {error} \\n\\n You must invoke the provided tool.\",\n )\n ]\n return {\n \"messages\": messages,\n \"context\": inputs[\"context\"],\n }\n\n\n# This will be run as a fallback chain\nfallback_chain = insert_errors | code_chain_claude_raw\nN = 3 # Max re-tries\ncode_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n fallbacks=[fallback_chain] * N, exception_key=\"error\"\n)\n\n\ndef parse_output(solution):\n \"\"\"When we add 'include_raw=True' to structured output,\n it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n\n return solution[\"parsed\"]\n\n\n# Optional: With re-try to correct for failure to invoke tool\ncode_gen_chain = code_gen_chain_re_try | parse_output\n\n# No re-try\ncode_gen_chain = code_gen_prompt_claude | structured_llm_claude | parse_output"] }, { "cell_type": "code", @@ -260,14 +92,7 @@ "scrolled": true }, "outputs": [], - "source": [ - "# Test\n", - "question = \"How do I build a RAG chain in LCEL?\"\n", - "solution = code_gen_chain.invoke(\n", - " {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n", - ")\n", - "solution" - ] + "source": ["# Test\nquestion = \"How do I build a RAG chain in LCEL?\"\nsolution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n)\nsolution"] }, { "cell_type": "markdown", @@ -285,26 +110,7 @@ "id": "c185f1a2-e943-4bed-b833-4243c9c64092", "metadata": {}, "outputs": [], - "source": [ - "from typing import List, TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " error : Binary flag for control flow to indicate whether test error was tripped\n", - " messages : With user question, error messages, reasoning\n", - " generation : Code solution\n", - " iterations : Number of tries\n", - " \"\"\"\n", - "\n", - " error: str\n", - " messages: List\n", - " generation: str\n", - " iterations: int" - ] + "source": ["from typing import List, TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n error : Binary flag for control flow to indicate whether test error was tripped\n messages : With user question, error messages, reasoning\n generation : Code solution\n iterations : Number of tries\n \"\"\"\n\n error: str\n messages: List\n generation: str\n iterations: int"] }, { "cell_type": "markdown", @@ -322,177 +128,7 @@ "id": "b70e8301-63ae-4f7e-ad8f-c9a052fe3566", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "### Parameter\n", - "\n", - "# Max tries\n", - "max_iterations = 3\n", - "# Reflect\n", - "# flag = 'reflect'\n", - "flag = \"do not reflect\"\n", - "\n", - "### Nodes\n", - "\n", - "\n", - "def generate(state: GraphState):\n", - " \"\"\"\n", - " Generate a code solution\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation\n", - " \"\"\"\n", - "\n", - " print(\"---GENERATING CODE SOLUTION---\")\n", - "\n", - " # State\n", - " messages = state[\"messages\"]\n", - " iterations = state[\"iterations\"]\n", - " error = state[\"error\"]\n", - "\n", - " # We have been routed back to generation with an error\n", - " if error == \"yes\":\n", - " messages += [\n", - " (\n", - " \"user\",\n", - " \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n", - " )\n", - " ]\n", - "\n", - " # Solution\n", - " code_solution = code_gen_chain.invoke(\n", - " {\"context\": concatenated_content, \"messages\": messages}\n", - " )\n", - " messages += [\n", - " (\n", - " \"assistant\",\n", - " f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n", - " )\n", - " ]\n", - "\n", - " # Increment\n", - " iterations = iterations + 1\n", - " return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n", - "\n", - "\n", - "def code_check(state: GraphState):\n", - " \"\"\"\n", - " Check code\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, error\n", - " \"\"\"\n", - "\n", - " print(\"---CHECKING CODE---\")\n", - "\n", - " # State\n", - " messages = state[\"messages\"]\n", - " code_solution = state[\"generation\"]\n", - " iterations = state[\"iterations\"]\n", - "\n", - " # Get solution components\n", - " imports = code_solution.imports\n", - " code = code_solution.code\n", - "\n", - " # Check imports\n", - " try:\n", - " exec(imports)\n", - " except Exception as e:\n", - " print(\"---CODE IMPORT CHECK: FAILED---\")\n", - " error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n", - " messages += error_message\n", - " return {\n", - " \"generation\": code_solution,\n", - " \"messages\": messages,\n", - " \"iterations\": iterations,\n", - " \"error\": \"yes\",\n", - " }\n", - "\n", - " # Check execution\n", - " try:\n", - " exec(imports + \"\\n\" + code)\n", - " except Exception as e:\n", - " print(\"---CODE BLOCK CHECK: FAILED---\")\n", - " error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n", - " messages += error_message\n", - " return {\n", - " \"generation\": code_solution,\n", - " \"messages\": messages,\n", - " \"iterations\": iterations,\n", - " \"error\": \"yes\",\n", - " }\n", - "\n", - " # No errors\n", - " print(\"---NO CODE TEST FAILURES---\")\n", - " return {\n", - " \"generation\": code_solution,\n", - " \"messages\": messages,\n", - " \"iterations\": iterations,\n", - " \"error\": \"no\",\n", - " }\n", - "\n", - "\n", - "def reflect(state: GraphState):\n", - " \"\"\"\n", - " Reflect on errors\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation\n", - " \"\"\"\n", - "\n", - " print(\"---GENERATING CODE SOLUTION---\")\n", - "\n", - " # State\n", - " messages = state[\"messages\"]\n", - " iterations = state[\"iterations\"]\n", - " code_solution = state[\"generation\"]\n", - "\n", - " # Prompt reflection\n", - "\n", - " # Add reflection\n", - " reflections = code_gen_chain.invoke(\n", - " {\"context\": concatenated_content, \"messages\": messages}\n", - " )\n", - " messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n", - " return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n", - "\n", - "\n", - "### Edges\n", - "\n", - "\n", - "def decide_to_finish(state: GraphState):\n", - " \"\"\"\n", - " Determines whether to finish.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Next node to call\n", - " \"\"\"\n", - " error = state[\"error\"]\n", - " iterations = state[\"iterations\"]\n", - "\n", - " if error == \"no\" or iterations == max_iterations:\n", - " print(\"---DECISION: FINISH---\")\n", - " return \"end\"\n", - " else:\n", - " print(\"---DECISION: RE-TRY SOLUTION---\")\n", - " if flag == \"reflect\":\n", - " return \"reflect\"\n", - " else:\n", - " return \"generate\"" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n### Parameter\n\n# Max tries\nmax_iterations = 3\n# Reflect\n# flag = 'reflect'\nflag = \"do not reflect\"\n\n### Nodes\n\n\ndef generate(state: GraphState):\n \"\"\"\n Generate a code solution\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n error = state[\"error\"]\n\n # We have been routed back to generation with an error\n if error == \"yes\":\n messages += [\n (\n \"user\",\n \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n\n # Solution\n code_solution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": messages}\n )\n messages += [\n (\n \"assistant\",\n f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n )\n ]\n\n # Increment\n iterations = iterations + 1\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\ndef code_check(state: GraphState):\n \"\"\"\n Check code\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, error\n \"\"\"\n\n print(\"---CHECKING CODE---\")\n\n # State\n messages = state[\"messages\"]\n code_solution = state[\"generation\"]\n iterations = state[\"iterations\"]\n\n # Get solution components\n imports = code_solution.imports\n code = code_solution.code\n\n # Check imports\n try:\n exec(imports)\n except Exception as e:\n print(\"---CODE IMPORT CHECK: FAILED---\")\n error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # Check execution\n try:\n exec(imports + \"\\n\" + code)\n except Exception as e:\n print(\"---CODE BLOCK CHECK: FAILED---\")\n error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # No errors\n print(\"---NO CODE TEST FAILURES---\")\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"no\",\n }\n\n\ndef reflect(state: GraphState):\n \"\"\"\n Reflect on errors\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n code_solution = state[\"generation\"]\n\n # Prompt reflection\n\n # Add reflection\n reflections = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": messages}\n )\n messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\n### Edges\n\n\ndef decide_to_finish(state: GraphState):\n \"\"\"\n Determines whether to finish.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n error = state[\"error\"]\n iterations = state[\"iterations\"]\n\n if error == \"no\" or iterations == max_iterations:\n print(\"---DECISION: FINISH---\")\n return \"end\"\n else:\n print(\"---DECISION: RE-TRY SOLUTION---\")\n if flag == \"reflect\":\n return \"reflect\"\n else:\n return \"generate\""] }, { "cell_type": "code", @@ -500,31 +136,7 @@ "id": "f66b4e00-4731-42c8-bc38-72dd0ff7c92c", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"generate\", generate) # generation solution\n", - "workflow.add_node(\"check_code\", code_check) # check code\n", - "workflow.add_node(\"reflect\", reflect) # reflect\n", - "\n", - "# Build graph\n", - "workflow.set_entry_point(\"generate\")\n", - "workflow.add_edge(\"generate\", \"check_code\")\n", - "workflow.add_conditional_edges(\n", - " \"check_code\",\n", - " decide_to_finish,\n", - " {\n", - " \"end\": END,\n", - " \"reflect\": \"reflect\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"reflect\", \"generate\")\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"generate\", generate) # generation solution\nworkflow.add_node(\"check_code\", code_check) # check code\nworkflow.add_node(\"reflect\", reflect) # reflect\n\n# Build graph\nworkflow.add_edge(START, \"generate\")\nworkflow.add_edge(\"generate\", \"check_code\")\nworkflow.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"reflect\": \"reflect\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"reflect\", \"generate\")\napp = workflow.compile()"] }, { "cell_type": "code", @@ -532,10 +144,7 @@ "id": "9bcaafe4-ddcf-4fab-8620-2d9b6c508f98", "metadata": {}, "outputs": [], - "source": [ - "question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\n", - "app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})" - ] + "source": ["question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\napp.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})"] }, { "cell_type": "markdown", @@ -563,11 +172,7 @@ "id": "678e8954-56b5-4cc6-be26-f7f2a060b242", "metadata": {}, "outputs": [], - "source": [ - "import langsmith\n", - "\n", - "client = langsmith.Client()" - ] + "source": ["import langsmith\n\nclient = langsmith.Client()"] }, { "cell_type": "code", @@ -575,13 +180,7 @@ "id": "ef7cf662-7a6f-4dee-965c-6309d4045feb", "metadata": {}, "outputs": [], - "source": [ - "# Clone the dataset to your tenant to use it\n", - "public_dataset = (\n", - " \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n", - ")\n", - "client.clone_public_dataset(public_dataset)" - ] + "source": ["# Clone the dataset to your tenant to use it\npublic_dataset = (\n \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n)\nclient.clone_public_dataset(public_dataset)"] }, { "cell_type": "markdown", @@ -597,28 +196,7 @@ "id": "455a34ea-52cb-4ae5-9f4a-7e4a08cd0c09", "metadata": {}, "outputs": [], - "source": [ - "from langsmith.schemas import Example, Run\n", - "\n", - "\n", - "def check_import(run: Run, example: Example) -> dict:\n", - " imports = run.outputs.get(\"imports\")\n", - " try:\n", - " exec(imports)\n", - " return {\"key\": \"import_check\", \"score\": 1}\n", - " except Exception:\n", - " return {\"key\": \"import_check\", \"score\": 0}\n", - "\n", - "\n", - "def check_execution(run: Run, example: Example) -> dict:\n", - " imports = run.outputs.get(\"imports\")\n", - " code = run.outputs.get(\"code\")\n", - " try:\n", - " exec(imports + \"\\n\" + code)\n", - " return {\"key\": \"code_execution_check\", \"score\": 1}\n", - " except Exception:\n", - " return {\"key\": \"code_execution_check\", \"score\": 0}" - ] + "source": ["from langsmith.schemas import Example, Run\n\n\ndef check_import(run: Run, example: Example) -> dict:\n imports = run.outputs.get(\"imports\")\n try:\n exec(imports)\n return {\"key\": \"import_check\", \"score\": 1}\n except Exception:\n return {\"key\": \"import_check\", \"score\": 0}\n\n\ndef check_execution(run: Run, example: Example) -> dict:\n imports = run.outputs.get(\"imports\")\n code = run.outputs.get(\"code\")\n try:\n exec(imports + \"\\n\" + code)\n return {\"key\": \"code_execution_check\", \"score\": 1}\n except Exception:\n return {\"key\": \"code_execution_check\", \"score\": 0}"] }, { "cell_type": "markdown", @@ -634,22 +212,7 @@ "id": "c8fa6bcb-b245-4422-b79a-582cd8a7d7ea", "metadata": {}, "outputs": [], - "source": [ - "def predict_base_case(example: dict):\n", - " \"\"\"Context stuffing\"\"\"\n", - " solution = code_gen_chain.invoke(\n", - " {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n", - " )\n", - " solution_structured = code_gen_chain.invoke([(\"code\", solution)])\n", - " return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n", - "\n", - "\n", - "def predict_langgraph(example: dict):\n", - " \"\"\"LangGraph\"\"\"\n", - " graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n", - " solution = graph[\"generation\"]\n", - " return {\"imports\": solution.imports, \"code\": solution.code}" - ] + "source": ["def predict_base_case(example: dict):\n \"\"\"Context stuffing\"\"\"\n solution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n )\n solution_structured = code_gen_chain.invoke([(\"code\", solution)])\n return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n\n\ndef predict_langgraph(example: dict):\n \"\"\"LangGraph\"\"\"\n graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n solution = graph[\"generation\"]\n return {\"imports\": solution.imports, \"code\": solution.code}"] }, { "cell_type": "code", @@ -657,15 +220,7 @@ "id": "d9c57468-97f6-47d6-a5e9-c09b53bfdd83", "metadata": {}, "outputs": [], - "source": [ - "from langsmith.evaluation import evaluate\n", - "\n", - "# Evaluator\n", - "code_evalulator = [check_import, check_execution]\n", - "\n", - "# Dataset\n", - "dataset_name = \"test-LCEL-code-gen\"" - ] + "source": ["from langsmith.evaluation import evaluate\n\n# Evaluator\ncode_evalulator = [check_import, check_execution]\n\n# Dataset\ndataset_name = \"test-LCEL-code-gen\""] }, { "cell_type": "code", @@ -673,19 +228,7 @@ "id": "2dacccf0-d73f-4017-aaf0-9806ffe5bd2c", "metadata": {}, "outputs": [], - "source": [ - "# Run base case\n", - "experiment_results_ = evaluate(\n", - " predict_base_case,\n", - " data=dataset_name,\n", - " evaluators=code_evalulator,\n", - " experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n", - " max_concurrency=2,\n", - " metadata={\n", - " \"llm\": expt_llm,\n", - " },\n", - ")" - ] + "source": ["# Run base case\nexperiment_results_ = evaluate(\n predict_base_case,\n data=dataset_name,\n evaluators=code_evalulator,\n experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n max_concurrency=2,\n metadata={\n \"llm\": expt_llm,\n },\n)"] }, { "cell_type": "code", @@ -693,20 +236,7 @@ "id": "71d90f9e-9dad-410c-a709-093d275029ae", "metadata": {}, "outputs": [], - "source": [ - "# Run with langgraph\n", - "experiment_results = evaluate(\n", - " predict_langgraph,\n", - " data=dataset_name,\n", - " evaluators=code_evalulator,\n", - " experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n", - " max_concurrency=2,\n", - " metadata={\n", - " \"llm\": expt_llm,\n", - " \"feedback\": flag,\n", - " },\n", - ")" - ] + "source": ["# Run with langgraph\nexperiment_results = evaluate(\n predict_langgraph,\n data=dataset_name,\n evaluators=code_evalulator,\n experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n max_concurrency=2,\n metadata={\n \"llm\": expt_llm,\n \"feedback\": flag,\n },\n)"] }, { "cell_type": "markdown", @@ -728,7 +258,7 @@ "id": "a42333c3-c098-4576-ae2a-0258de64ece2", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/code_assistant/langgraph_code_assistant_mistral.ipynb b/examples/code_assistant/langgraph_code_assistant_mistral.ipynb index 0533444b9..fe947f11b 100644 --- a/examples/code_assistant/langgraph_code_assistant_mistral.ipynb +++ b/examples/code_assistant/langgraph_code_assistant_mistral.ipynb @@ -33,9 +33,7 @@ "id": "e501686f-323f-4b87-8f9c-8ba89133078b", "metadata": {}, "outputs": [], - "source": [ - "! pip install -U langchain_community langchain-mistralai langchain langgraph" - ] + "source": ["! pip install -U langchain_community langchain-mistralai langchain langgraph"] }, { "cell_type": "markdown", @@ -53,12 +51,7 @@ "id": "982e4609-86e4-4934-828f-e03d89c20393", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\n", - "mistral_api_key = os.getenv(\"MISTRAL_API_KEY\") # Ensure this is set" - ] + "source": ["import os\n\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\nmistral_api_key = os.getenv(\"MISTRAL_API_KEY\") # Ensure this is set"] }, { "cell_type": "markdown", @@ -76,12 +69,7 @@ "id": "37b172d2-3a9d-49a8-898c-22ed0cb45c88", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"Mistral-code-gen-testing\"" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Mistral-code-gen-testing\""] }, { "cell_type": "markdown", @@ -99,42 +87,7 @@ "id": "a188c8ca-c053-4e6d-b7af-38a3b6b371c7", "metadata": {}, "outputs": [], - "source": [ - "# Select LLM\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_mistralai import ChatMistralAI\n", - "\n", - "mistral_model = \"mistral-large-latest\"\n", - "llm = ChatMistralAI(model=mistral_model, temperature=0)\n", - "\n", - "# Prompt\n", - "code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"You are a coding assistant. Ensure any code you provide can be executed with all required imports and variables \\n\n", - " defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block.\n", - " \\n Here is the user question:\"\"\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ")\n", - "\n", - "\n", - "# Data model\n", - "class code(BaseModel):\n", - " \"\"\"Code output\"\"\"\n", - "\n", - " prefix: str = Field(description=\"Description of the problem and approach\")\n", - " imports: str = Field(description=\"Code block import statements\")\n", - " code: str = Field(description=\"Code block not including import statements\")\n", - " description = \"Schema for code solutions to questions about LCEL.\"\n", - "\n", - "\n", - "# LLM\n", - "code_gen_chain = llm.with_structured_output(code, include_raw=False)" - ] + "source": ["# Select LLM\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_mistralai import ChatMistralAI\n\nmistral_model = \"mistral-large-latest\"\nllm = ChatMistralAI(model=mistral_model, temperature=0)\n\n# Prompt\ncode_gen_prompt_claude = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a coding assistant. Ensure any code you provide can be executed with all required imports and variables \\n\n defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block.\n \\n Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\n# LLM\ncode_gen_chain = llm.with_structured_output(code, include_raw=False)"] }, { "cell_type": "code", @@ -142,10 +95,7 @@ "id": "9fc0290d-5a04-4514-8664-91f9dbf2da7b", "metadata": {}, "outputs": [], - "source": [ - "question = \"Write a function for fibonacci.\"\n", - "messages = [(\"user\", question)]" - ] + "source": ["question = \"Write a function for fibonacci.\"\nmessages = [(\"user\", question)]"] }, { "cell_type": "code", @@ -164,11 +114,7 @@ "output_type": "execute_result" } ], - "source": [ - "# Test\n", - "result = code_gen_chain.invoke(messages)\n", - "result" - ] + "source": ["# Test\nresult = code_gen_chain.invoke(messages)\nresult"] }, { "cell_type": "markdown", @@ -184,28 +130,7 @@ "id": "183d77b8-f180-4815-b39f-8ef507ec0534", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated, TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " error : Binary flag for control flow to indicate whether test error was tripped\n", - " messages : With user question, error messages, reasoning\n", - " generation : Code solution\n", - " iterations : Number of tries\n", - " \"\"\"\n", - "\n", - " error: str\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " generation: str\n", - " iterations: int" - ] + "source": ["from typing import Annotated, TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n error : Binary flag for control flow to indicate whether test error was tripped\n messages : With user question, error messages, reasoning\n generation : Code solution\n iterations : Number of tries\n \"\"\"\n\n error: str\n messages: Annotated[list[AnyMessage], add_messages]\n generation: str\n iterations: int"] }, { "cell_type": "markdown", @@ -221,163 +146,7 @@ "id": "14bc89d1-3ca6-4847-a048-1803e0e4600e", "metadata": {}, "outputs": [], - "source": [ - "import uuid\n", - "\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "### Parameters\n", - "max_iterations = 3\n", - "\n", - "\n", - "### Nodes\n", - "def generate(state: GraphState):\n", - " \"\"\"\n", - " Generate a code solution\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation\n", - " \"\"\"\n", - "\n", - " print(\"---GENERATING CODE SOLUTION---\")\n", - "\n", - " # State\n", - " messages = state[\"messages\"]\n", - " iterations = state[\"iterations\"]\n", - "\n", - " # Solution\n", - " code_solution = code_gen_chain.invoke(messages)\n", - " messages += [\n", - " (\n", - " \"assistant\",\n", - " f\"Here is my attempt to solve the problem: {code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n", - " )\n", - " ]\n", - "\n", - " # Increment\n", - " iterations = iterations + 1\n", - " return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n", - "\n", - "\n", - "def code_check(state: GraphState):\n", - " \"\"\"\n", - " Check code\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, error\n", - " \"\"\"\n", - "\n", - " print(\"---CHECKING CODE---\")\n", - "\n", - " # State\n", - " messages = state[\"messages\"]\n", - " code_solution = state[\"generation\"]\n", - " iterations = state[\"iterations\"]\n", - "\n", - " # Get solution components\n", - " imports = code_solution.imports\n", - " code = code_solution.code\n", - "\n", - " # Check imports\n", - " try:\n", - " exec(imports)\n", - " except Exception as e:\n", - " print(\"---CODE IMPORT CHECK: FAILED---\")\n", - " error_message = [\n", - " (\n", - " \"user\",\n", - " f\"Your solution failed the import test. Here is the error: {e}. Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n", - " )\n", - " ]\n", - " messages += error_message\n", - " return {\n", - " \"generation\": code_solution,\n", - " \"messages\": messages,\n", - " \"iterations\": iterations,\n", - " \"error\": \"yes\",\n", - " }\n", - "\n", - " # Check execution\n", - " try:\n", - " combined_code = f\"{imports}\\n{code}\"\n", - " print(f\"CODE TO TEST: {combined_code}\")\n", - " # Use a shared scope for exec\n", - " global_scope = {}\n", - " exec(combined_code, global_scope)\n", - " except Exception as e:\n", - " print(\"---CODE BLOCK CHECK: FAILED---\")\n", - " error_message = [\n", - " (\n", - " \"user\",\n", - " f\"Your solution failed the code execution test: {e}) Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n", - " )\n", - " ]\n", - " messages += error_message\n", - " return {\n", - " \"generation\": code_solution,\n", - " \"messages\": messages,\n", - " \"iterations\": iterations,\n", - " \"error\": \"yes\",\n", - " }\n", - "\n", - " # No errors\n", - " print(\"---NO CODE TEST FAILURES---\")\n", - " return {\n", - " \"generation\": code_solution,\n", - " \"messages\": messages,\n", - " \"iterations\": iterations,\n", - " \"error\": \"no\",\n", - " }\n", - "\n", - "\n", - "### Conditional edges\n", - "\n", - "\n", - "def decide_to_finish(state: GraphState):\n", - " \"\"\"\n", - " Determines whether to finish.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Next node to call\n", - " \"\"\"\n", - " error = state[\"error\"]\n", - " iterations = state[\"iterations\"]\n", - "\n", - " if error == \"no\" or iterations == max_iterations:\n", - " print(\"---DECISION: FINISH---\")\n", - " return \"end\"\n", - " else:\n", - " print(\"---DECISION: RE-TRY SOLUTION---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "### Utilities\n", - "\n", - "\n", - "def _print_event(event: dict, _printed: set, max_length=1500):\n", - " current_state = event.get(\"dialog_state\")\n", - " if current_state:\n", - " print(\"Currently in: \", current_state[-1])\n", - " message = event.get(\"messages\")\n", - " if message:\n", - " if isinstance(message, list):\n", - " message = message[-1]\n", - " if message.id not in _printed:\n", - " msg_repr = message.pretty_repr(html=True)\n", - " if len(msg_repr) > max_length:\n", - " msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n", - " print(msg_repr)\n", - " _printed.add(message.id)" - ] + "source": ["import uuid\n\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n### Parameters\nmax_iterations = 3\n\n\n### Nodes\ndef generate(state: GraphState):\n \"\"\"\n Generate a code solution\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n\n # Solution\n code_solution = code_gen_chain.invoke(messages)\n messages += [\n (\n \"assistant\",\n f\"Here is my attempt to solve the problem: {code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n )\n ]\n\n # Increment\n iterations = iterations + 1\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\ndef code_check(state: GraphState):\n \"\"\"\n Check code\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, error\n \"\"\"\n\n print(\"---CHECKING CODE---\")\n\n # State\n messages = state[\"messages\"]\n code_solution = state[\"generation\"]\n iterations = state[\"iterations\"]\n\n # Get solution components\n imports = code_solution.imports\n code = code_solution.code\n\n # Check imports\n try:\n exec(imports)\n except Exception as e:\n print(\"---CODE IMPORT CHECK: FAILED---\")\n error_message = [\n (\n \"user\",\n f\"Your solution failed the import test. Here is the error: {e}. Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # Check execution\n try:\n combined_code = f\"{imports}\\n{code}\"\n print(f\"CODE TO TEST: {combined_code}\")\n # Use a shared scope for exec\n global_scope = {}\n exec(combined_code, global_scope)\n except Exception as e:\n print(\"---CODE BLOCK CHECK: FAILED---\")\n error_message = [\n (\n \"user\",\n f\"Your solution failed the code execution test: {e}) Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # No errors\n print(\"---NO CODE TEST FAILURES---\")\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"no\",\n }\n\n\n### Conditional edges\n\n\ndef decide_to_finish(state: GraphState):\n \"\"\"\n Determines whether to finish.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n error = state[\"error\"]\n iterations = state[\"iterations\"]\n\n if error == \"no\" or iterations == max_iterations:\n print(\"---DECISION: FINISH---\")\n return \"end\"\n else:\n print(\"---DECISION: RE-TRY SOLUTION---\")\n return \"generate\"\n\n\n### Utilities\n\n\ndef _print_event(event: dict, _printed: set, max_length=1500):\n current_state = event.get(\"dialog_state\")\n if current_state:\n print(\"Currently in: \", current_state[-1])\n message = event.get(\"messages\")\n if message:\n if isinstance(message, list):\n message = message[-1]\n if message.id not in _printed:\n msg_repr = message.pretty_repr(html=True)\n if len(msg_repr) > max_length:\n msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n print(msg_repr)\n _printed.add(message.id)"] }, { "cell_type": "code", @@ -385,31 +154,7 @@ "id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "builder = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "builder.add_node(\"generate\", generate) # generation solution\n", - "builder.add_node(\"check_code\", code_check) # check code\n", - "\n", - "# Build graph\n", - "builder.set_entry_point(\"generate\")\n", - "builder.add_edge(\"generate\", \"check_code\")\n", - "builder.add_conditional_edges(\n", - " \"check_code\",\n", - " decide_to_finish,\n", - " {\n", - " \"end\": END,\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "graph = builder.compile(checkpointer=memory)" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=memory)"] }, { "cell_type": "code", @@ -428,15 +173,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "code", @@ -444,23 +181,7 @@ "id": "242aa2f0-2c31-462f-a958-ff9ae0cf7c62", "metadata": {}, "outputs": [], - "source": [ - "_printed = set()\n", - "thread_id = str(uuid.uuid4())\n", - "config = {\n", - " \"configurable\": {\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "question = \"Write a Python program that prints 'Hello, World!' to the console.\"\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " _print_event(event, _printed)" - ] + "source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"Write a Python program that prints 'Hello, World!' to the console.\"\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"] }, { "cell_type": "markdown", @@ -478,31 +199,7 @@ "id": "390b2768-f395-4aea-8b0e-9d36212a31ac", "metadata": {}, "outputs": [], - "source": [ - "_printed = set()\n", - "thread_id = str(uuid.uuid4())\n", - "config = {\n", - " \"configurable\": {\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "question = \"\"\"Create a Python program that checks if a given string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward (ignoring spaces, punctuation, and capitalization).\n", - "\n", - "Requirements:\n", - "The program should define a function is_palindrome(s) that takes a string s as input.\n", - "The function should return True if the string is a palindrome and False otherwise.\n", - "Ignore spaces, punctuation, and case differences when checking for palindromes.\n", - "\n", - "Give an example of it working on an example input word.\"\"\"\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " _print_event(event, _printed)" - ] + "source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Create a Python program that checks if a given string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward (ignoring spaces, punctuation, and capitalization).\n\nRequirements:\nThe program should define a function is_palindrome(s) that takes a string s as input.\nThe function should return True if the string is a palindrome and False otherwise.\nIgnore spaces, punctuation, and case differences when checking for palindromes.\n\nGive an example of it working on an example input word.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"] }, { "cell_type": "markdown", @@ -520,26 +217,7 @@ "id": "0a3f946b-e2f2-44d9-905b-09f36980cf9f", "metadata": {}, "outputs": [], - "source": [ - "_printed = set()\n", - "thread_id = str(uuid.uuid4())\n", - "config = {\n", - " \"configurable\": {\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "question = \"\"\"Write a program that prints the numbers from 1 to 100. \n", - "But for multiples of three, print \"Fizz\" instead of the number, and for the multiples of five, print \"Buzz\". \n", - "For numbers which are multiples of both three and five, print \"FizzBuzz\".\"\"\"\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " _print_event(event, _printed)" - ] + "source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Write a program that prints the numbers from 1 to 100. \nBut for multiples of three, print \"Fizz\" instead of the number, and for the multiples of five, print \"Buzz\". \nFor numbers which are multiples of both three and five, print \"FizzBuzz\".\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"] }, { "cell_type": "markdown", @@ -557,37 +235,7 @@ "id": "2bb883df-540b-46ab-9415-fe27db68456f", "metadata": {}, "outputs": [], - "source": [ - "import uuid\n", - "\n", - "_printed = set()\n", - "thread_id = str(uuid.uuid4())\n", - "config = {\n", - " \"configurable\": {\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "question = \"\"\"I want to vectorize a function\n", - "\n", - " frame = np.zeros((out_h, out_w, 3), dtype=np.uint8)\n", - " for i, val1 in enumerate(rows):\n", - " for j, val2 in enumerate(cols):\n", - " for j, val3 in enumerate(ch):\n", - " # Assuming you want to store the pair as tuples in the matrix\n", - " frame[i, j, k] = image[val1, val2, val3]\n", - "\n", - " out.write(np.array(frame))\n", - "\n", - "with a simple numpy function that does something like this what is it called. Show me a test case with this working.\"\"\"\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " _print_event(event, _printed)" - ] + "source": ["import uuid\n\n_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"I want to vectorize a function\n\n frame = np.zeros((out_h, out_w, 3), dtype=np.uint8)\n for i, val1 in enumerate(rows):\n for j, val2 in enumerate(cols):\n for j, val3 in enumerate(ch):\n # Assuming you want to store the pair as tuples in the matrix\n frame[i, j, k] = image[val1, val2, val3]\n\n out.write(np.array(frame))\n\nwith a simple numpy function that does something like this what is it called. Show me a test case with this working.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"] }, { "cell_type": "markdown", @@ -605,34 +253,7 @@ "id": "ee05da1f-c272-405d-8a7b-552cfc3106e1", "metadata": {}, "outputs": [], - "source": [ - "_printed = set()\n", - "thread_id = str(uuid.uuid4())\n", - "config = {\n", - " \"configurable\": {\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "question = \"\"\"Create a Python program that allows two players to play a game of Tic-Tac-Toe. The game should be played on a 3x3 grid. The program should:\n", - "\n", - "- Allow players to take turns to input their moves.\n", - "- Check for invalid moves (e.g., placing a marker on an already occupied space).\n", - "- Determine and announce the winner or if the game ends in a draw.\n", - "\n", - "Requirements:\n", - "- Use a 2D list to represent the Tic-Tac-Toe board.\n", - "- Use functions to modularize the code.\n", - "- Validate player input.\n", - "- Check for win conditions and draw conditions after each move.\"\"\"\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " _print_event(event, _printed)" - ] + "source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Create a Python program that allows two players to play a game of Tic-Tac-Toe. The game should be played on a 3x3 grid. The program should:\n\n- Allow players to take turns to input their moves.\n- Check for invalid moves (e.g., placing a marker on an already occupied space).\n- Determine and announce the winner or if the game ends in a draw.\n\nRequirements:\n- Use a 2D list to represent the Tic-Tac-Toe board.\n- Use functions to modularize the code.\n- Validate player input.\n- Check for win conditions and draw conditions after each move.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"] }, { "cell_type": "markdown", @@ -650,7 +271,7 @@ "id": "814fc2a4-8e5b-4faa-8f52-3977226bd09a", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/configuration.ipynb b/examples/configuration.ipynb index c29269c01..4a2817c21 100644 --- a/examples/configuration.ipynb +++ b/examples/configuration.ipynb @@ -28,35 +28,7 @@ "id": "816523d0-0b59-47cf-9f4c-4838024efe22", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.messages import BaseMessage, HumanMessage\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "model = ChatAnthropic(model_name=\"claude-2.1\")\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]\n", - "\n", - "\n", - "def _call_model(state):\n", - " response = model.invoke(state[\"messages\"])\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "workflow.add_node(\"model\", _call_model)\n", - "workflow.set_entry_point(\"model\")\n", - "workflow.add_edge(\"model\", END)\n", - "\n", - "app = workflow.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.messages import BaseMessage, HumanMessage\n\nfrom langgraph.graph import END, StateGraph, START\n\nmodel = ChatAnthropic(model_name=\"claude-2.1\")\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n\n\ndef _call_model(state):\n response = model.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] }, { "cell_type": "code", @@ -76,9 +48,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" - ] + "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] }, { "cell_type": "markdown", @@ -98,31 +68,7 @@ "id": "c01f1e7c-8e8b-4e26-98f7-56ac225077b4", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "openai_model = ChatOpenAI()\n", - "\n", - "models = {\n", - " \"anthropic\": model,\n", - " \"openai\": openai_model,\n", - "}\n", - "\n", - "\n", - "def _call_model(state, config):\n", - " m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n", - " response = m.invoke(state[\"messages\"])\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "workflow.add_node(\"model\", _call_model)\n", - "workflow.set_entry_point(\"model\")\n", - "workflow.add_edge(\"model\", END)\n", - "\n", - "app = workflow.compile()" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nopenai_model = ChatOpenAI()\n\nmodels = {\n \"anthropic\": model,\n \"openai\": openai_model,\n}\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n response = m.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] }, { "cell_type": "markdown", @@ -150,9 +96,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" - ] + "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] }, { "cell_type": "markdown", @@ -180,10 +124,7 @@ "output_type": "execute_result" } ], - "source": [ - "config = {\"configurable\": {\"model\": \"openai\"}}\n", - "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)" - ] + "source": ["config = {\"configurable\": {\"model\": \"openai\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"] }, { "cell_type": "markdown", @@ -199,29 +140,7 @@ "id": "f0393a43-9fbe-4056-972f-3e91ea329041", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import SystemMessage\n", - "\n", - "\n", - "def _call_model(state, config):\n", - " m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n", - " messages = state[\"messages\"]\n", - " if \"system_message\" in config[\"configurable\"]:\n", - " messages = [\n", - " SystemMessage(content=config[\"configurable\"][\"system_message\"])\n", - " ] + messages\n", - " response = m.invoke(messages)\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "workflow.add_node(\"model\", _call_model)\n", - "workflow.set_entry_point(\"model\")\n", - "workflow.add_edge(\"model\", END)\n", - "\n", - "app = workflow.compile()" - ] + "source": ["from langchain_core.messages import SystemMessage\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n messages = state[\"messages\"]\n if \"system_message\" in config[\"configurable\"]:\n messages = [\n SystemMessage(content=config[\"configurable\"][\"system_message\"])\n ] + messages\n response = m.invoke(messages)\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] }, { "cell_type": "code", @@ -241,9 +160,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" - ] + "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] }, { "cell_type": "code", @@ -263,10 +180,7 @@ "output_type": "execute_result" } ], - "source": [ - "config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\n", - "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)" - ] + "source": ["config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"] }, { "cell_type": "code", @@ -274,7 +188,7 @@ "id": "a5c5f7f4-4b0e-4cde-93a6-c1c6329b8591", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/customer-support/customer-support.ipynb b/examples/customer-support/customer-support.ipynb index 75bd46d7b..3715ce4f0 100644 --- a/examples/customer-support/customer-support.ipynb +++ b/examples/customer-support/customer-support.ipynb @@ -32,10 +32,7 @@ "id": "afc570bf-e129-415b-8f2d-8bbce08131ab", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas" - ] + "source": ["%%capture --no-stderr\n% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas"] }, { "cell_type": "code", @@ -43,24 +40,7 @@ "id": "358e5666-b7c5-4e46-90a1-7ea273d86ee3", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")\n", - "_set_env(\"TAVILY_API_KEY\")\n", - "\n", - "# Recommended\n", - "_set_env(\"LANGCHAIN_API_KEY\")\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"Customer Support Bot Tutorial\"" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")\n\n# Recommended\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Customer Support Bot Tutorial\""] }, { "cell_type": "markdown", @@ -78,68 +58,7 @@ "id": "71638c2a-5038-439e-907a-de2bb548db34", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "import shutil\n", - "import sqlite3\n", - "\n", - "import pandas as pd\n", - "import requests\n", - "\n", - "db_url = \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/travel2.sqlite\"\n", - "local_file = \"travel2.sqlite\"\n", - "# The backup lets us restart for each tutorial section\n", - "backup_file = \"travel2.backup.sqlite\"\n", - "overwrite = False\n", - "if overwrite or not os.path.exists(local_file):\n", - " response = requests.get(db_url)\n", - " response.raise_for_status() # Ensure the request was successful\n", - " with open(local_file, \"wb\") as f:\n", - " f.write(response.content)\n", - " # Backup - we will use this to \"reset\" our DB in each section\n", - " shutil.copy(local_file, backup_file)\n", - "# Convert the flights to present time for our tutorial\n", - "conn = sqlite3.connect(local_file)\n", - "cursor = conn.cursor()\n", - "\n", - "tables = pd.read_sql(\n", - " \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n", - ").name.tolist()\n", - "tdf = {}\n", - "for t in tables:\n", - " tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n", - "\n", - "example_time = pd.to_datetime(\n", - " tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n", - ").max()\n", - "current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n", - "time_diff = current_time - example_time\n", - "\n", - "tdf[\"bookings\"][\"book_date\"] = (\n", - " pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n", - " + time_diff\n", - ")\n", - "\n", - "datetime_columns = [\n", - " \"scheduled_departure\",\n", - " \"scheduled_arrival\",\n", - " \"actual_departure\",\n", - " \"actual_arrival\",\n", - "]\n", - "for column in datetime_columns:\n", - " tdf[\"flights\"][column] = (\n", - " pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n", - " )\n", - "\n", - "for table_name, df in tdf.items():\n", - " df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n", - "del df\n", - "del tdf\n", - "conn.commit()\n", - "conn.close()\n", - "\n", - "db = local_file # We'll be using this local file as our DB in this tutorial" - ] + "source": ["import os\nimport shutil\nimport sqlite3\n\nimport pandas as pd\nimport requests\n\ndb_url = \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/travel2.sqlite\"\nlocal_file = \"travel2.sqlite\"\n# The backup lets us restart for each tutorial section\nbackup_file = \"travel2.backup.sqlite\"\noverwrite = False\nif overwrite or not os.path.exists(local_file):\n response = requests.get(db_url)\n response.raise_for_status() # Ensure the request was successful\n with open(local_file, \"wb\") as f:\n f.write(response.content)\n # Backup - we will use this to \"reset\" our DB in each section\n shutil.copy(local_file, backup_file)\n# Convert the flights to present time for our tutorial\nconn = sqlite3.connect(local_file)\ncursor = conn.cursor()\n\ntables = pd.read_sql(\n \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n).name.tolist()\ntdf = {}\nfor t in tables:\n tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n\nexample_time = pd.to_datetime(\n tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n).max()\ncurrent_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\ntime_diff = current_time - example_time\n\ntdf[\"bookings\"][\"book_date\"] = (\n pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n + time_diff\n)\n\ndatetime_columns = [\n \"scheduled_departure\",\n \"scheduled_arrival\",\n \"actual_departure\",\n \"actual_arrival\",\n]\nfor column in datetime_columns:\n tdf[\"flights\"][column] = (\n pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n )\n\nfor table_name, df in tdf.items():\n df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\ndel df\ndel tdf\nconn.commit()\nconn.close()\n\ndb = local_file # We'll be using this local file as our DB in this tutorial"] }, { "cell_type": "markdown", @@ -162,59 +81,7 @@ "id": "654e2f81", "metadata": {}, "outputs": [], - "source": [ - "import re\n", - "\n", - "import numpy as np\n", - "import openai\n", - "from langchain_core.tools import tool\n", - "\n", - "response = requests.get(\n", - " \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/swiss_faq.md\"\n", - ")\n", - "response.raise_for_status()\n", - "faq_text = response.text\n", - "\n", - "docs = [{\"page_content\": txt} for txt in re.split(r\"(?=\\n##)\", faq_text)]\n", - "\n", - "\n", - "class VectorStoreRetriever:\n", - " def __init__(self, docs: list, vectors: list, oai_client):\n", - " self._arr = np.array(vectors)\n", - " self._docs = docs\n", - " self._client = oai_client\n", - "\n", - " @classmethod\n", - " def from_docs(cls, docs, oai_client):\n", - " embeddings = oai_client.embeddings.create(\n", - " model=\"text-embedding-3-small\", input=[doc[\"page_content\"] for doc in docs]\n", - " )\n", - " vectors = [emb.embedding for emb in embeddings.data]\n", - " return cls(docs, vectors, oai_client)\n", - "\n", - " def query(self, query: str, k: int = 5) -> list[dict]:\n", - " embed = self._client.embeddings.create(\n", - " model=\"text-embedding-3-small\", input=[query]\n", - " )\n", - " # \"@\" is just a matrix multiplication in python\n", - " scores = np.array(embed.data[0].embedding) @ self._arr.T\n", - " top_k_idx = np.argpartition(scores, -k)[-k:]\n", - " top_k_idx_sorted = top_k_idx[np.argsort(-scores[top_k_idx])]\n", - " return [\n", - " {**self._docs[idx], \"similarity\": scores[idx]} for idx in top_k_idx_sorted\n", - " ]\n", - "\n", - "\n", - "retriever = VectorStoreRetriever.from_docs(docs, openai.Client())\n", - "\n", - "\n", - "@tool\n", - "def lookup_policy(query: str) -> str:\n", - " \"\"\"Consult the company policies to check whether certain options are permitted.\n", - " Use this before making any flight changes performing other 'write' events.\"\"\"\n", - " docs = retriever.query(query, k=2)\n", - " return \"\\n\\n\".join([doc[\"page_content\"] for doc in docs])" - ] + "source": ["import re\n\nimport numpy as np\nimport openai\nfrom langchain_core.tools import tool\n\nresponse = requests.get(\n \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/swiss_faq.md\"\n)\nresponse.raise_for_status()\nfaq_text = response.text\n\ndocs = [{\"page_content\": txt} for txt in re.split(r\"(?=\\n##)\", faq_text)]\n\n\nclass VectorStoreRetriever:\n def __init__(self, docs: list, vectors: list, oai_client):\n self._arr = np.array(vectors)\n self._docs = docs\n self._client = oai_client\n\n @classmethod\n def from_docs(cls, docs, oai_client):\n embeddings = oai_client.embeddings.create(\n model=\"text-embedding-3-small\", input=[doc[\"page_content\"] for doc in docs]\n )\n vectors = [emb.embedding for emb in embeddings.data]\n return cls(docs, vectors, oai_client)\n\n def query(self, query: str, k: int = 5) -> list[dict]:\n embed = self._client.embeddings.create(\n model=\"text-embedding-3-small\", input=[query]\n )\n # \"@\" is just a matrix multiplication in python\n scores = np.array(embed.data[0].embedding) @ self._arr.T\n top_k_idx = np.argpartition(scores, -k)[-k:]\n top_k_idx_sorted = top_k_idx[np.argsort(-scores[top_k_idx])]\n return [\n {**self._docs[idx], \"similarity\": scores[idx]} for idx in top_k_idx_sorted\n ]\n\n\nretriever = VectorStoreRetriever.from_docs(docs, openai.Client())\n\n\n@tool\ndef lookup_policy(query: str) -> str:\n \"\"\"Consult the company policies to check whether certain options are permitted.\n Use this before making any flight changes performing other 'write' events.\"\"\"\n docs = retriever.query(query, k=2)\n return \"\\n\\n\".join([doc[\"page_content\"] for doc in docs])"] }, { "cell_type": "markdown", @@ -234,205 +101,7 @@ "id": "043b4341", "metadata": {}, "outputs": [], - "source": [ - "import sqlite3\n", - "from datetime import date, datetime\n", - "from typing import Optional\n", - "\n", - "import pytz\n", - "from langchain_core.runnables import ensure_config\n", - "\n", - "\n", - "@tool\n", - "def fetch_user_flight_information() -> list[dict]:\n", - " \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n", - "\n", - " Returns:\n", - " A list of dictionaries where each dictionary contains the ticket details,\n", - " associated flight details, and the seat assignments for each ticket belonging to the user.\n", - " \"\"\"\n", - " config = ensure_config() # Fetch from the context\n", - " configuration = config.get(\"configurable\", {})\n", - " passenger_id = configuration.get(\"passenger_id\", None)\n", - " if not passenger_id:\n", - " raise ValueError(\"No passenger ID configured.\")\n", - "\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " query = \"\"\"\n", - " SELECT \n", - " t.ticket_no, t.book_ref,\n", - " f.flight_id, f.flight_no, f.departure_airport, f.arrival_airport, f.scheduled_departure, f.scheduled_arrival,\n", - " bp.seat_no, tf.fare_conditions\n", - " FROM \n", - " tickets t\n", - " JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no\n", - " JOIN flights f ON tf.flight_id = f.flight_id\n", - " JOIN boarding_passes bp ON bp.ticket_no = t.ticket_no AND bp.flight_id = f.flight_id\n", - " WHERE \n", - " t.passenger_id = ?\n", - " \"\"\"\n", - " cursor.execute(query, (passenger_id,))\n", - " rows = cursor.fetchall()\n", - " column_names = [column[0] for column in cursor.description]\n", - " results = [dict(zip(column_names, row)) for row in rows]\n", - "\n", - " cursor.close()\n", - " conn.close()\n", - "\n", - " return results\n", - "\n", - "\n", - "@tool\n", - "def search_flights(\n", - " departure_airport: Optional[str] = None,\n", - " arrival_airport: Optional[str] = None,\n", - " start_time: Optional[date | datetime] = None,\n", - " end_time: Optional[date | datetime] = None,\n", - " limit: int = 20,\n", - ") -> list[dict]:\n", - " \"\"\"Search for flights based on departure airport, arrival airport, and departure time range.\"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " query = \"SELECT * FROM flights WHERE 1 = 1\"\n", - " params = []\n", - "\n", - " if departure_airport:\n", - " query += \" AND departure_airport = ?\"\n", - " params.append(departure_airport)\n", - "\n", - " if arrival_airport:\n", - " query += \" AND arrival_airport = ?\"\n", - " params.append(arrival_airport)\n", - "\n", - " if start_time:\n", - " query += \" AND scheduled_departure >= ?\"\n", - " params.append(start_time)\n", - "\n", - " if end_time:\n", - " query += \" AND scheduled_departure <= ?\"\n", - " params.append(end_time)\n", - " query += \" LIMIT ?\"\n", - " params.append(limit)\n", - " cursor.execute(query, params)\n", - " rows = cursor.fetchall()\n", - " column_names = [column[0] for column in cursor.description]\n", - " results = [dict(zip(column_names, row)) for row in rows]\n", - "\n", - " cursor.close()\n", - " conn.close()\n", - "\n", - " return results\n", - "\n", - "\n", - "@tool\n", - "def update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n", - " \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n", - " config = ensure_config()\n", - " configuration = config.get(\"configurable\", {})\n", - " passenger_id = configuration.get(\"passenger_id\", None)\n", - " if not passenger_id:\n", - " raise ValueError(\"No passenger ID configured.\")\n", - "\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\n", - " \"SELECT departure_airport, arrival_airport, scheduled_departure FROM flights WHERE flight_id = ?\",\n", - " (new_flight_id,),\n", - " )\n", - " new_flight = cursor.fetchone()\n", - " if not new_flight:\n", - " cursor.close()\n", - " conn.close()\n", - " return \"Invalid new flight ID provided.\"\n", - " column_names = [column[0] for column in cursor.description]\n", - " new_flight_dict = dict(zip(column_names, new_flight))\n", - " timezone = pytz.timezone(\"Etc/GMT-3\")\n", - " current_time = datetime.now(tz=timezone)\n", - " departure_time = datetime.strptime(\n", - " new_flight_dict[\"scheduled_departure\"], \"%Y-%m-%d %H:%M:%S.%f%z\"\n", - " )\n", - " time_until = (departure_time - current_time).total_seconds()\n", - " if time_until < (3 * 3600):\n", - " return f\"Not permitted to reschedule to a flight that is less than 3 hours from the current time. Selected flight is at {departure_time}.\"\n", - "\n", - " cursor.execute(\n", - " \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n", - " )\n", - " current_flight = cursor.fetchone()\n", - " if not current_flight:\n", - " cursor.close()\n", - " conn.close()\n", - " return \"No existing ticket found for the given ticket number.\"\n", - "\n", - " # Check the signed-in user actually has this ticket\n", - " cursor.execute(\n", - " \"SELECT * FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n", - " (ticket_no, passenger_id),\n", - " )\n", - " current_ticket = cursor.fetchone()\n", - " if not current_ticket:\n", - " cursor.close()\n", - " conn.close()\n", - " return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n", - "\n", - " # In a real application, you'd likely add additional checks here to enforce business logic,\n", - " # like \"does the new departure airport match the current ticket\", etc.\n", - " # While it's best to try to be *proactive* in 'type-hinting' policies to the LLM\n", - " # it's inevitably going to get things wrong, so you **also** need to ensure your\n", - " # API enforces valid behavior\n", - " cursor.execute(\n", - " \"UPDATE ticket_flights SET flight_id = ? WHERE ticket_no = ?\",\n", - " (new_flight_id, ticket_no),\n", - " )\n", - " conn.commit()\n", - "\n", - " cursor.close()\n", - " conn.close()\n", - " return \"Ticket successfully updated to new flight.\"\n", - "\n", - "\n", - "@tool\n", - "def cancel_ticket(ticket_no: str) -> str:\n", - " \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n", - " config = ensure_config()\n", - " configuration = config.get(\"configurable\", {})\n", - " passenger_id = configuration.get(\"passenger_id\", None)\n", - " if not passenger_id:\n", - " raise ValueError(\"No passenger ID configured.\")\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\n", - " \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n", - " )\n", - " existing_ticket = cursor.fetchone()\n", - " if not existing_ticket:\n", - " cursor.close()\n", - " conn.close()\n", - " return \"No existing ticket found for the given ticket number.\"\n", - "\n", - " # Check the signed-in user actually has this ticket\n", - " cursor.execute(\n", - " \"SELECT flight_id FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n", - " (ticket_no, passenger_id),\n", - " )\n", - " current_ticket = cursor.fetchone()\n", - " if not current_ticket:\n", - " cursor.close()\n", - " conn.close()\n", - " return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n", - "\n", - " cursor.execute(\"DELETE FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,))\n", - " conn.commit()\n", - "\n", - " cursor.close()\n", - " conn.close()\n", - " return \"Ticket successfully cancelled.\"" - ] + "source": ["import sqlite3\nfrom datetime import date, datetime\nfrom typing import Optional\n\nimport pytz\nfrom langchain_core.runnables import ensure_config\n\n\n@tool\ndef fetch_user_flight_information() -> list[dict]:\n \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n\n Returns:\n A list of dictionaries where each dictionary contains the ticket details,\n associated flight details, and the seat assignments for each ticket belonging to the user.\n \"\"\"\n config = ensure_config() # Fetch from the context\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n if not passenger_id:\n raise ValueError(\"No passenger ID configured.\")\n\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"\"\"\n SELECT \n t.ticket_no, t.book_ref,\n f.flight_id, f.flight_no, f.departure_airport, f.arrival_airport, f.scheduled_departure, f.scheduled_arrival,\n bp.seat_no, tf.fare_conditions\n FROM \n tickets t\n JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no\n JOIN flights f ON tf.flight_id = f.flight_id\n JOIN boarding_passes bp ON bp.ticket_no = t.ticket_no AND bp.flight_id = f.flight_id\n WHERE \n t.passenger_id = ?\n \"\"\"\n cursor.execute(query, (passenger_id,))\n rows = cursor.fetchall()\n column_names = [column[0] for column in cursor.description]\n results = [dict(zip(column_names, row)) for row in rows]\n\n cursor.close()\n conn.close()\n\n return results\n\n\n@tool\ndef search_flights(\n departure_airport: Optional[str] = None,\n arrival_airport: Optional[str] = None,\n start_time: Optional[date | datetime] = None,\n end_time: Optional[date | datetime] = None,\n limit: int = 20,\n) -> list[dict]:\n \"\"\"Search for flights based on departure airport, arrival airport, and departure time range.\"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM flights WHERE 1 = 1\"\n params = []\n\n if departure_airport:\n query += \" AND departure_airport = ?\"\n params.append(departure_airport)\n\n if arrival_airport:\n query += \" AND arrival_airport = ?\"\n params.append(arrival_airport)\n\n if start_time:\n query += \" AND scheduled_departure >= ?\"\n params.append(start_time)\n\n if end_time:\n query += \" AND scheduled_departure <= ?\"\n params.append(end_time)\n query += \" LIMIT ?\"\n params.append(limit)\n cursor.execute(query, params)\n rows = cursor.fetchall()\n column_names = [column[0] for column in cursor.description]\n results = [dict(zip(column_names, row)) for row in rows]\n\n cursor.close()\n conn.close()\n\n return results\n\n\n@tool\ndef update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n config = ensure_config()\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n if not passenger_id:\n raise ValueError(\"No passenger ID configured.\")\n\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"SELECT departure_airport, arrival_airport, scheduled_departure FROM flights WHERE flight_id = ?\",\n (new_flight_id,),\n )\n new_flight = cursor.fetchone()\n if not new_flight:\n cursor.close()\n conn.close()\n return \"Invalid new flight ID provided.\"\n column_names = [column[0] for column in cursor.description]\n new_flight_dict = dict(zip(column_names, new_flight))\n timezone = pytz.timezone(\"Etc/GMT-3\")\n current_time = datetime.now(tz=timezone)\n departure_time = datetime.strptime(\n new_flight_dict[\"scheduled_departure\"], \"%Y-%m-%d %H:%M:%S.%f%z\"\n )\n time_until = (departure_time - current_time).total_seconds()\n if time_until < (3 * 3600):\n return f\"Not permitted to reschedule to a flight that is less than 3 hours from the current time. Selected flight is at {departure_time}.\"\n\n cursor.execute(\n \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n )\n current_flight = cursor.fetchone()\n if not current_flight:\n cursor.close()\n conn.close()\n return \"No existing ticket found for the given ticket number.\"\n\n # Check the signed-in user actually has this ticket\n cursor.execute(\n \"SELECT * FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n (ticket_no, passenger_id),\n )\n current_ticket = cursor.fetchone()\n if not current_ticket:\n cursor.close()\n conn.close()\n return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n\n # In a real application, you'd likely add additional checks here to enforce business logic,\n # like \"does the new departure airport match the current ticket\", etc.\n # While it's best to try to be *proactive* in 'type-hinting' policies to the LLM\n # it's inevitably going to get things wrong, so you **also** need to ensure your\n # API enforces valid behavior\n cursor.execute(\n \"UPDATE ticket_flights SET flight_id = ? WHERE ticket_no = ?\",\n (new_flight_id, ticket_no),\n )\n conn.commit()\n\n cursor.close()\n conn.close()\n return \"Ticket successfully updated to new flight.\"\n\n\n@tool\ndef cancel_ticket(ticket_no: str) -> str:\n \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n config = ensure_config()\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n if not passenger_id:\n raise ValueError(\"No passenger ID configured.\")\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n )\n existing_ticket = cursor.fetchone()\n if not existing_ticket:\n cursor.close()\n conn.close()\n return \"No existing ticket found for the given ticket number.\"\n\n # Check the signed-in user actually has this ticket\n cursor.execute(\n \"SELECT flight_id FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n (ticket_no, passenger_id),\n )\n current_ticket = cursor.fetchone()\n if not current_ticket:\n cursor.close()\n conn.close()\n return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n\n cursor.execute(\"DELETE FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,))\n conn.commit()\n\n cursor.close()\n conn.close()\n return \"Ticket successfully cancelled.\""] }, { "cell_type": "markdown", @@ -450,145 +119,7 @@ "id": "f3edabaf-7a23-4f9f-9c57-97b799bc21df", "metadata": {}, "outputs": [], - "source": [ - "from datetime import date, datetime\n", - "from typing import Optional, Union\n", - "\n", - "\n", - "@tool\n", - "def search_car_rentals(\n", - " location: Optional[str] = None,\n", - " name: Optional[str] = None,\n", - " price_tier: Optional[str] = None,\n", - " start_date: Optional[Union[datetime, date]] = None,\n", - " end_date: Optional[Union[datetime, date]] = None,\n", - ") -> list[dict]:\n", - " \"\"\"\n", - " Search for car rentals based on location, name, price tier, start date, and end date.\n", - "\n", - " Args:\n", - " location (Optional[str]): The location of the car rental. Defaults to None.\n", - " name (Optional[str]): The name of the car rental company. Defaults to None.\n", - " price_tier (Optional[str]): The price tier of the car rental. Defaults to None.\n", - " start_date (Optional[Union[datetime, date]]): The start date of the car rental. Defaults to None.\n", - " end_date (Optional[Union[datetime, date]]): The end date of the car rental. Defaults to None.\n", - "\n", - " Returns:\n", - " list[dict]: A list of car rental dictionaries matching the search criteria.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " query = \"SELECT * FROM car_rentals WHERE 1=1\"\n", - " params = []\n", - "\n", - " if location:\n", - " query += \" AND location LIKE ?\"\n", - " params.append(f\"%{location}%\")\n", - " if name:\n", - " query += \" AND name LIKE ?\"\n", - " params.append(f\"%{name}%\")\n", - " # For our tutorial, we will let you match on any dates and price tier.\n", - " # (since our toy dataset doesn't have much data)\n", - " cursor.execute(query, params)\n", - " results = cursor.fetchall()\n", - "\n", - " conn.close()\n", - "\n", - " return [\n", - " dict(zip([column[0] for column in cursor.description], row)) for row in results\n", - " ]\n", - "\n", - "\n", - "@tool\n", - "def book_car_rental(rental_id: int) -> str:\n", - " \"\"\"\n", - " Book a car rental by its ID.\n", - "\n", - " Args:\n", - " rental_id (int): The ID of the car rental to book.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the car rental was successfully booked or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\"UPDATE car_rentals SET booked = 1 WHERE id = ?\", (rental_id,))\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Car rental {rental_id} successfully booked.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No car rental found with ID {rental_id}.\"\n", - "\n", - "\n", - "@tool\n", - "def update_car_rental(\n", - " rental_id: int,\n", - " start_date: Optional[Union[datetime, date]] = None,\n", - " end_date: Optional[Union[datetime, date]] = None,\n", - ") -> str:\n", - " \"\"\"\n", - " Update a car rental's start and end dates by its ID.\n", - "\n", - " Args:\n", - " rental_id (int): The ID of the car rental to update.\n", - " start_date (Optional[Union[datetime, date]]): The new start date of the car rental. Defaults to None.\n", - " end_date (Optional[Union[datetime, date]]): The new end date of the car rental. Defaults to None.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the car rental was successfully updated or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " if start_date:\n", - " cursor.execute(\n", - " \"UPDATE car_rentals SET start_date = ? WHERE id = ?\",\n", - " (start_date, rental_id),\n", - " )\n", - " if end_date:\n", - " cursor.execute(\n", - " \"UPDATE car_rentals SET end_date = ? WHERE id = ?\", (end_date, rental_id)\n", - " )\n", - "\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Car rental {rental_id} successfully updated.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No car rental found with ID {rental_id}.\"\n", - "\n", - "\n", - "@tool\n", - "def cancel_car_rental(rental_id: int) -> str:\n", - " \"\"\"\n", - " Cancel a car rental by its ID.\n", - "\n", - " Args:\n", - " rental_id (int): The ID of the car rental to cancel.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the car rental was successfully cancelled or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\"UPDATE car_rentals SET booked = 0 WHERE id = ?\", (rental_id,))\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Car rental {rental_id} successfully cancelled.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No car rental found with ID {rental_id}.\"" - ] + "source": ["from datetime import date, datetime\nfrom typing import Optional, Union\n\n\n@tool\ndef search_car_rentals(\n location: Optional[str] = None,\n name: Optional[str] = None,\n price_tier: Optional[str] = None,\n start_date: Optional[Union[datetime, date]] = None,\n end_date: Optional[Union[datetime, date]] = None,\n) -> list[dict]:\n \"\"\"\n Search for car rentals based on location, name, price tier, start date, and end date.\n\n Args:\n location (Optional[str]): The location of the car rental. Defaults to None.\n name (Optional[str]): The name of the car rental company. Defaults to None.\n price_tier (Optional[str]): The price tier of the car rental. Defaults to None.\n start_date (Optional[Union[datetime, date]]): The start date of the car rental. Defaults to None.\n end_date (Optional[Union[datetime, date]]): The end date of the car rental. Defaults to None.\n\n Returns:\n list[dict]: A list of car rental dictionaries matching the search criteria.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM car_rentals WHERE 1=1\"\n params = []\n\n if location:\n query += \" AND location LIKE ?\"\n params.append(f\"%{location}%\")\n if name:\n query += \" AND name LIKE ?\"\n params.append(f\"%{name}%\")\n # For our tutorial, we will let you match on any dates and price tier.\n # (since our toy dataset doesn't have much data)\n cursor.execute(query, params)\n results = cursor.fetchall()\n\n conn.close()\n\n return [\n dict(zip([column[0] for column in cursor.description], row)) for row in results\n ]\n\n\n@tool\ndef book_car_rental(rental_id: int) -> str:\n \"\"\"\n Book a car rental by its ID.\n\n Args:\n rental_id (int): The ID of the car rental to book.\n\n Returns:\n str: A message indicating whether the car rental was successfully booked or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE car_rentals SET booked = 1 WHERE id = ?\", (rental_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Car rental {rental_id} successfully booked.\"\n else:\n conn.close()\n return f\"No car rental found with ID {rental_id}.\"\n\n\n@tool\ndef update_car_rental(\n rental_id: int,\n start_date: Optional[Union[datetime, date]] = None,\n end_date: Optional[Union[datetime, date]] = None,\n) -> str:\n \"\"\"\n Update a car rental's start and end dates by its ID.\n\n Args:\n rental_id (int): The ID of the car rental to update.\n start_date (Optional[Union[datetime, date]]): The new start date of the car rental. Defaults to None.\n end_date (Optional[Union[datetime, date]]): The new end date of the car rental. Defaults to None.\n\n Returns:\n str: A message indicating whether the car rental was successfully updated or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n if start_date:\n cursor.execute(\n \"UPDATE car_rentals SET start_date = ? WHERE id = ?\",\n (start_date, rental_id),\n )\n if end_date:\n cursor.execute(\n \"UPDATE car_rentals SET end_date = ? WHERE id = ?\", (end_date, rental_id)\n )\n\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Car rental {rental_id} successfully updated.\"\n else:\n conn.close()\n return f\"No car rental found with ID {rental_id}.\"\n\n\n@tool\ndef cancel_car_rental(rental_id: int) -> str:\n \"\"\"\n Cancel a car rental by its ID.\n\n Args:\n rental_id (int): The ID of the car rental to cancel.\n\n Returns:\n str: A message indicating whether the car rental was successfully cancelled or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE car_rentals SET booked = 0 WHERE id = ?\", (rental_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Car rental {rental_id} successfully cancelled.\"\n else:\n conn.close()\n return f\"No car rental found with ID {rental_id}.\""] }, { "cell_type": "markdown", @@ -606,140 +137,7 @@ "id": "a8e4ab3c-0086-4257-855b-97cc4037513f", "metadata": {}, "outputs": [], - "source": [ - "@tool\n", - "def search_hotels(\n", - " location: Optional[str] = None,\n", - " name: Optional[str] = None,\n", - " price_tier: Optional[str] = None,\n", - " checkin_date: Optional[Union[datetime, date]] = None,\n", - " checkout_date: Optional[Union[datetime, date]] = None,\n", - ") -> list[dict]:\n", - " \"\"\"\n", - " Search for hotels based on location, name, price tier, check-in date, and check-out date.\n", - "\n", - " Args:\n", - " location (Optional[str]): The location of the hotel. Defaults to None.\n", - " name (Optional[str]): The name of the hotel. Defaults to None.\n", - " price_tier (Optional[str]): The price tier of the hotel. Defaults to None. Examples: Midscale, Upper Midscale, Upscale, Luxury\n", - " checkin_date (Optional[Union[datetime, date]]): The check-in date of the hotel. Defaults to None.\n", - " checkout_date (Optional[Union[datetime, date]]): The check-out date of the hotel. Defaults to None.\n", - "\n", - " Returns:\n", - " list[dict]: A list of hotel dictionaries matching the search criteria.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " query = \"SELECT * FROM hotels WHERE 1=1\"\n", - " params = []\n", - "\n", - " if location:\n", - " query += \" AND location LIKE ?\"\n", - " params.append(f\"%{location}%\")\n", - " if name:\n", - " query += \" AND name LIKE ?\"\n", - " params.append(f\"%{name}%\")\n", - " # For the sake of this tutorial, we will let you match on any dates and price tier.\n", - " cursor.execute(query, params)\n", - " results = cursor.fetchall()\n", - "\n", - " conn.close()\n", - "\n", - " return [\n", - " dict(zip([column[0] for column in cursor.description], row)) for row in results\n", - " ]\n", - "\n", - "\n", - "@tool\n", - "def book_hotel(hotel_id: int) -> str:\n", - " \"\"\"\n", - " Book a hotel by its ID.\n", - "\n", - " Args:\n", - " hotel_id (int): The ID of the hotel to book.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the hotel was successfully booked or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\"UPDATE hotels SET booked = 1 WHERE id = ?\", (hotel_id,))\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Hotel {hotel_id} successfully booked.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No hotel found with ID {hotel_id}.\"\n", - "\n", - "\n", - "@tool\n", - "def update_hotel(\n", - " hotel_id: int,\n", - " checkin_date: Optional[Union[datetime, date]] = None,\n", - " checkout_date: Optional[Union[datetime, date]] = None,\n", - ") -> str:\n", - " \"\"\"\n", - " Update a hotel's check-in and check-out dates by its ID.\n", - "\n", - " Args:\n", - " hotel_id (int): The ID of the hotel to update.\n", - " checkin_date (Optional[Union[datetime, date]]): The new check-in date of the hotel. Defaults to None.\n", - " checkout_date (Optional[Union[datetime, date]]): The new check-out date of the hotel. Defaults to None.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the hotel was successfully updated or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " if checkin_date:\n", - " cursor.execute(\n", - " \"UPDATE hotels SET checkin_date = ? WHERE id = ?\", (checkin_date, hotel_id)\n", - " )\n", - " if checkout_date:\n", - " cursor.execute(\n", - " \"UPDATE hotels SET checkout_date = ? WHERE id = ?\",\n", - " (checkout_date, hotel_id),\n", - " )\n", - "\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Hotel {hotel_id} successfully updated.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No hotel found with ID {hotel_id}.\"\n", - "\n", - "\n", - "@tool\n", - "def cancel_hotel(hotel_id: int) -> str:\n", - " \"\"\"\n", - " Cancel a hotel by its ID.\n", - "\n", - " Args:\n", - " hotel_id (int): The ID of the hotel to cancel.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the hotel was successfully cancelled or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\"UPDATE hotels SET booked = 0 WHERE id = ?\", (hotel_id,))\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Hotel {hotel_id} successfully cancelled.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No hotel found with ID {hotel_id}.\"" - ] + "source": ["@tool\ndef search_hotels(\n location: Optional[str] = None,\n name: Optional[str] = None,\n price_tier: Optional[str] = None,\n checkin_date: Optional[Union[datetime, date]] = None,\n checkout_date: Optional[Union[datetime, date]] = None,\n) -> list[dict]:\n \"\"\"\n Search for hotels based on location, name, price tier, check-in date, and check-out date.\n\n Args:\n location (Optional[str]): The location of the hotel. Defaults to None.\n name (Optional[str]): The name of the hotel. Defaults to None.\n price_tier (Optional[str]): The price tier of the hotel. Defaults to None. Examples: Midscale, Upper Midscale, Upscale, Luxury\n checkin_date (Optional[Union[datetime, date]]): The check-in date of the hotel. Defaults to None.\n checkout_date (Optional[Union[datetime, date]]): The check-out date of the hotel. Defaults to None.\n\n Returns:\n list[dict]: A list of hotel dictionaries matching the search criteria.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM hotels WHERE 1=1\"\n params = []\n\n if location:\n query += \" AND location LIKE ?\"\n params.append(f\"%{location}%\")\n if name:\n query += \" AND name LIKE ?\"\n params.append(f\"%{name}%\")\n # For the sake of this tutorial, we will let you match on any dates and price tier.\n cursor.execute(query, params)\n results = cursor.fetchall()\n\n conn.close()\n\n return [\n dict(zip([column[0] for column in cursor.description], row)) for row in results\n ]\n\n\n@tool\ndef book_hotel(hotel_id: int) -> str:\n \"\"\"\n Book a hotel by its ID.\n\n Args:\n hotel_id (int): The ID of the hotel to book.\n\n Returns:\n str: A message indicating whether the hotel was successfully booked or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE hotels SET booked = 1 WHERE id = ?\", (hotel_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Hotel {hotel_id} successfully booked.\"\n else:\n conn.close()\n return f\"No hotel found with ID {hotel_id}.\"\n\n\n@tool\ndef update_hotel(\n hotel_id: int,\n checkin_date: Optional[Union[datetime, date]] = None,\n checkout_date: Optional[Union[datetime, date]] = None,\n) -> str:\n \"\"\"\n Update a hotel's check-in and check-out dates by its ID.\n\n Args:\n hotel_id (int): The ID of the hotel to update.\n checkin_date (Optional[Union[datetime, date]]): The new check-in date of the hotel. Defaults to None.\n checkout_date (Optional[Union[datetime, date]]): The new check-out date of the hotel. Defaults to None.\n\n Returns:\n str: A message indicating whether the hotel was successfully updated or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n if checkin_date:\n cursor.execute(\n \"UPDATE hotels SET checkin_date = ? WHERE id = ?\", (checkin_date, hotel_id)\n )\n if checkout_date:\n cursor.execute(\n \"UPDATE hotels SET checkout_date = ? WHERE id = ?\",\n (checkout_date, hotel_id),\n )\n\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Hotel {hotel_id} successfully updated.\"\n else:\n conn.close()\n return f\"No hotel found with ID {hotel_id}.\"\n\n\n@tool\ndef cancel_hotel(hotel_id: int) -> str:\n \"\"\"\n Cancel a hotel by its ID.\n\n Args:\n hotel_id (int): The ID of the hotel to cancel.\n\n Returns:\n str: A message indicating whether the hotel was successfully cancelled or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE hotels SET booked = 0 WHERE id = ?\", (hotel_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Hotel {hotel_id} successfully cancelled.\"\n else:\n conn.close()\n return f\"No hotel found with ID {hotel_id}.\""] }, { "cell_type": "markdown", @@ -757,134 +155,7 @@ "id": "2260eccb-8ae2-4a41-a1ba-f78ee3df3010", "metadata": {}, "outputs": [], - "source": [ - "@tool\n", - "def search_trip_recommendations(\n", - " location: Optional[str] = None,\n", - " name: Optional[str] = None,\n", - " keywords: Optional[str] = None,\n", - ") -> list[dict]:\n", - " \"\"\"\n", - " Search for trip recommendations based on location, name, and keywords.\n", - "\n", - " Args:\n", - " location (Optional[str]): The location of the trip recommendation. Defaults to None.\n", - " name (Optional[str]): The name of the trip recommendation. Defaults to None.\n", - " keywords (Optional[str]): The keywords associated with the trip recommendation. Defaults to None.\n", - "\n", - " Returns:\n", - " list[dict]: A list of trip recommendation dictionaries matching the search criteria.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " query = \"SELECT * FROM trip_recommendations WHERE 1=1\"\n", - " params = []\n", - "\n", - " if location:\n", - " query += \" AND location LIKE ?\"\n", - " params.append(f\"%{location}%\")\n", - " if name:\n", - " query += \" AND name LIKE ?\"\n", - " params.append(f\"%{name}%\")\n", - " if keywords:\n", - " keyword_list = keywords.split(\",\")\n", - " keyword_conditions = \" OR \".join([\"keywords LIKE ?\" for _ in keyword_list])\n", - " query += f\" AND ({keyword_conditions})\"\n", - " params.extend([f\"%{keyword.strip()}%\" for keyword in keyword_list])\n", - "\n", - " cursor.execute(query, params)\n", - " results = cursor.fetchall()\n", - "\n", - " conn.close()\n", - "\n", - " return [\n", - " dict(zip([column[0] for column in cursor.description], row)) for row in results\n", - " ]\n", - "\n", - "\n", - "@tool\n", - "def book_excursion(recommendation_id: int) -> str:\n", - " \"\"\"\n", - " Book a excursion by its recommendation ID.\n", - "\n", - " Args:\n", - " recommendation_id (int): The ID of the trip recommendation to book.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the trip recommendation was successfully booked or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\n", - " \"UPDATE trip_recommendations SET booked = 1 WHERE id = ?\", (recommendation_id,)\n", - " )\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Trip recommendation {recommendation_id} successfully booked.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No trip recommendation found with ID {recommendation_id}.\"\n", - "\n", - "\n", - "@tool\n", - "def update_excursion(recommendation_id: int, details: str) -> str:\n", - " \"\"\"\n", - " Update a trip recommendation's details by its ID.\n", - "\n", - " Args:\n", - " recommendation_id (int): The ID of the trip recommendation to update.\n", - " details (str): The new details of the trip recommendation.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the trip recommendation was successfully updated or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\n", - " \"UPDATE trip_recommendations SET details = ? WHERE id = ?\",\n", - " (details, recommendation_id),\n", - " )\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Trip recommendation {recommendation_id} successfully updated.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No trip recommendation found with ID {recommendation_id}.\"\n", - "\n", - "\n", - "@tool\n", - "def cancel_excursion(recommendation_id: int) -> str:\n", - " \"\"\"\n", - " Cancel a trip recommendation by its ID.\n", - "\n", - " Args:\n", - " recommendation_id (int): The ID of the trip recommendation to cancel.\n", - "\n", - " Returns:\n", - " str: A message indicating whether the trip recommendation was successfully cancelled or not.\n", - " \"\"\"\n", - " conn = sqlite3.connect(db)\n", - " cursor = conn.cursor()\n", - "\n", - " cursor.execute(\n", - " \"UPDATE trip_recommendations SET booked = 0 WHERE id = ?\", (recommendation_id,)\n", - " )\n", - " conn.commit()\n", - "\n", - " if cursor.rowcount > 0:\n", - " conn.close()\n", - " return f\"Trip recommendation {recommendation_id} successfully cancelled.\"\n", - " else:\n", - " conn.close()\n", - " return f\"No trip recommendation found with ID {recommendation_id}.\"" - ] + "source": ["@tool\ndef search_trip_recommendations(\n location: Optional[str] = None,\n name: Optional[str] = None,\n keywords: Optional[str] = None,\n) -> list[dict]:\n \"\"\"\n Search for trip recommendations based on location, name, and keywords.\n\n Args:\n location (Optional[str]): The location of the trip recommendation. Defaults to None.\n name (Optional[str]): The name of the trip recommendation. Defaults to None.\n keywords (Optional[str]): The keywords associated with the trip recommendation. Defaults to None.\n\n Returns:\n list[dict]: A list of trip recommendation dictionaries matching the search criteria.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM trip_recommendations WHERE 1=1\"\n params = []\n\n if location:\n query += \" AND location LIKE ?\"\n params.append(f\"%{location}%\")\n if name:\n query += \" AND name LIKE ?\"\n params.append(f\"%{name}%\")\n if keywords:\n keyword_list = keywords.split(\",\")\n keyword_conditions = \" OR \".join([\"keywords LIKE ?\" for _ in keyword_list])\n query += f\" AND ({keyword_conditions})\"\n params.extend([f\"%{keyword.strip()}%\" for keyword in keyword_list])\n\n cursor.execute(query, params)\n results = cursor.fetchall()\n\n conn.close()\n\n return [\n dict(zip([column[0] for column in cursor.description], row)) for row in results\n ]\n\n\n@tool\ndef book_excursion(recommendation_id: int) -> str:\n \"\"\"\n Book a excursion by its recommendation ID.\n\n Args:\n recommendation_id (int): The ID of the trip recommendation to book.\n\n Returns:\n str: A message indicating whether the trip recommendation was successfully booked or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"UPDATE trip_recommendations SET booked = 1 WHERE id = ?\", (recommendation_id,)\n )\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Trip recommendation {recommendation_id} successfully booked.\"\n else:\n conn.close()\n return f\"No trip recommendation found with ID {recommendation_id}.\"\n\n\n@tool\ndef update_excursion(recommendation_id: int, details: str) -> str:\n \"\"\"\n Update a trip recommendation's details by its ID.\n\n Args:\n recommendation_id (int): The ID of the trip recommendation to update.\n details (str): The new details of the trip recommendation.\n\n Returns:\n str: A message indicating whether the trip recommendation was successfully updated or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"UPDATE trip_recommendations SET details = ? WHERE id = ?\",\n (details, recommendation_id),\n )\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Trip recommendation {recommendation_id} successfully updated.\"\n else:\n conn.close()\n return f\"No trip recommendation found with ID {recommendation_id}.\"\n\n\n@tool\ndef cancel_excursion(recommendation_id: int) -> str:\n \"\"\"\n Cancel a trip recommendation by its ID.\n\n Args:\n recommendation_id (int): The ID of the trip recommendation to cancel.\n\n Returns:\n str: A message indicating whether the trip recommendation was successfully cancelled or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"UPDATE trip_recommendations SET booked = 0 WHERE id = ?\", (recommendation_id,)\n )\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Trip recommendation {recommendation_id} successfully cancelled.\"\n else:\n conn.close()\n return f\"No trip recommendation found with ID {recommendation_id}.\""] }, { "cell_type": "markdown", @@ -902,48 +173,7 @@ "id": "663f001e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "from langchain_core.runnables import RunnableLambda\n", - "\n", - "from langgraph.prebuilt import ToolNode\n", - "\n", - "\n", - "def handle_tool_error(state) -> dict:\n", - " error = state.get(\"error\")\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " return {\n", - " \"messages\": [\n", - " ToolMessage(\n", - " content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc in tool_calls\n", - " ]\n", - " }\n", - "\n", - "\n", - "def create_tool_node_with_fallback(tools: list) -> dict:\n", - " return ToolNode(tools).with_fallbacks(\n", - " [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n", - " )\n", - "\n", - "\n", - "def _print_event(event: dict, _printed: set, max_length=1500):\n", - " current_state = event.get(\"dialog_state\")\n", - " if current_state:\n", - " print(\"Currently in: \", current_state[-1])\n", - " message = event.get(\"messages\")\n", - " if message:\n", - " if isinstance(message, list):\n", - " message = message[-1]\n", - " if message.id not in _printed:\n", - " msg_repr = message.pretty_repr(html=True)\n", - " if len(msg_repr) > max_length:\n", - " msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n", - " print(msg_repr)\n", - " _printed.add(message.id)" - ] + "source": ["from langchain_core.messages import ToolMessage\nfrom langchain_core.runnables import RunnableLambda\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef handle_tool_error(state) -> dict:\n error = state.get(\"error\")\n tool_calls = state[\"messages\"][-1].tool_calls\n return {\n \"messages\": [\n ToolMessage(\n content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n tool_call_id=tc[\"id\"],\n )\n for tc in tool_calls\n ]\n }\n\n\ndef create_tool_node_with_fallback(tools: list) -> dict:\n return ToolNode(tools).with_fallbacks(\n [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n )\n\n\ndef _print_event(event: dict, _printed: set, max_length=1500):\n current_state = event.get(\"dialog_state\")\n if current_state:\n print(\"Currently in: \", current_state[-1])\n message = event.get(\"messages\")\n if message:\n if isinstance(message, list):\n message = message[-1]\n if message.id not in _printed:\n msg_repr = message.pretty_repr(html=True)\n if len(msg_repr) > max_length:\n msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n print(msg_repr)\n _printed.add(message.id)"] }, { "cell_type": "markdown", @@ -973,17 +203,7 @@ "id": "a3216948", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list[AnyMessage], add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]"] }, { "cell_type": "markdown", @@ -1010,83 +230,7 @@ ] } ], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.runnables import Runnable, RunnableConfig\n", - "\n", - "\n", - "class Assistant:\n", - " def __init__(self, runnable: Runnable):\n", - " self.runnable = runnable\n", - "\n", - " def __call__(self, state: State, config: RunnableConfig):\n", - " while True:\n", - " configuration = config.get(\"configurable\", {})\n", - " passenger_id = configuration.get(\"passenger_id\", None)\n", - " state = {**state, \"user_info\": passenger_id}\n", - " result = self.runnable.invoke(state)\n", - " # If the LLM happens to return an empty response, we will re-prompt it\n", - " # for an actual response.\n", - " if not result.tool_calls and (\n", - " not result.content\n", - " or isinstance(result.content, list)\n", - " and not result.content[0].get(\"text\")\n", - " ):\n", - " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n", - " state = {**state, \"messages\": messages}\n", - " else:\n", - " break\n", - " return {\"messages\": result}\n", - "\n", - "\n", - "# Haiku is faster and cheaper, but less accurate\n", - "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n", - "# You could swap LLMs, though you will likely want to update the prompts when\n", - "# doing so!\n", - "# from langchain_openai import ChatOpenAI\n", - "\n", - "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", - "\n", - "primary_assistant_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a helpful customer support assistant for Swiss Airlines. \"\n", - " \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \" If a search comes up empty, expand your search before giving up.\"\n", - " \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n", - " \"\\nCurrent time: {time}.\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "part_1_tools = [\n", - " TavilySearchResults(max_results=1),\n", - " fetch_user_flight_information,\n", - " search_flights,\n", - " lookup_policy,\n", - " update_ticket_to_new_flight,\n", - " cancel_ticket,\n", - " search_car_rentals,\n", - " book_car_rental,\n", - " update_car_rental,\n", - " cancel_car_rental,\n", - " search_hotels,\n", - " book_hotel,\n", - " update_hotel,\n", - " cancel_hotel,\n", - " search_trip_recommendations,\n", - " book_excursion,\n", - " update_excursion,\n", - " cancel_excursion,\n", - "]\n", - "part_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)" - ] + "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import Runnable, RunnableConfig\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n state = {**state, \"user_info\": passenger_id}\n result = self.runnable.invoke(state)\n # If the LLM happens to return an empty response, we will re-prompt it\n # for an actual response.\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\n# Haiku is faster and cheaper, but less accurate\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n# You could swap LLMs, though you will likely want to update the prompts when\n# doing so!\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nprimary_assistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\npart_1_tools = [\n TavilySearchResults(max_results=1),\n fetch_user_flight_information,\n search_flights,\n lookup_policy,\n update_ticket_to_new_flight,\n cancel_ticket,\n search_car_rentals,\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n search_hotels,\n book_hotel,\n update_hotel,\n cancel_hotel,\n search_trip_recommendations,\n book_excursion,\n update_excursion,\n cancel_excursion,\n]\npart_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)"] }, { "cell_type": "markdown", @@ -1104,30 +248,7 @@ "id": "36064ee6", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.prebuilt import tools_condition\n", - "\n", - "builder = StateGraph(State)\n", - "\n", - "\n", - "# Define nodes: these do the work\n", - "builder.add_node(\"assistant\", Assistant(part_1_assistant_runnable))\n", - "builder.add_node(\"tools\", create_tool_node_with_fallback(part_1_tools))\n", - "# Define edges: these determine how the control flow moves\n", - "builder.set_entry_point(\"assistant\")\n", - "builder.add_conditional_edges(\n", - " \"assistant\",\n", - " tools_condition,\n", - ")\n", - "builder.add_edge(\"tools\", \"assistant\")\n", - "\n", - "# The checkpointer lets the graph persist its state\n", - "# this is a complete memory for the entire graph.\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "part_1_graph = builder.compile(checkpointer=memory)" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\n# Define nodes: these do the work\nbuilder.add_node(\"assistant\", Assistant(part_1_assistant_runnable))\nbuilder.add_node(\"tools\", create_tool_node_with_fallback(part_1_tools))\n# Define edges: these determine how the control flow moves\nbuilder.add_edge(START, \"assistant\")\nbuilder.add_conditional_edges(\n \"assistant\",\n tools_condition,\n)\nbuilder.add_edge(\"tools\", \"assistant\")\n\n# The checkpointer lets the graph persist its state\n# this is a complete memory for the entire graph.\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_1_graph = builder.compile(checkpointer=memory)"] }, { "cell_type": "code", @@ -1146,15 +267,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -1721,51 +834,7 @@ ] } ], - "source": [ - "import shutil\n", - "import uuid\n", - "\n", - "# Let's create an example conversation a user might have with the assistant\n", - "tutorial_questions = [\n", - " \"Hi there, what time is my flight?\",\n", - " \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n", - " \"Update my flight to sometime next week then\",\n", - " \"The next available option is great\",\n", - " \"what about lodging and transportation?\",\n", - " \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n", - " \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n", - " \"yes go ahead and book anything that's moderate expense and has availability.\",\n", - " \"Now for a car, what are my options?\",\n", - " \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n", - " \"Cool so now what recommendations do you have on excursions?\",\n", - " \"Are they available while I'm there?\",\n", - " \"interesting - i like the museums, what options are there? \",\n", - " \"OK great pick one and book it for my second day there.\",\n", - "]\n", - "\n", - "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", - "thread_id = str(uuid.uuid4())\n", - "\n", - "config = {\n", - " \"configurable\": {\n", - " # The passenger_id is used in our flight tools to\n", - " # fetch the user's flight information\n", - " \"passenger_id\": \"3442 587242\",\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "\n", - "_printed = set()\n", - "for question in tutorial_questions:\n", - " events = part_1_graph.stream(\n", - " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n", - " )\n", - " for event in events:\n", - " _print_event(event, _printed)" - ] + "source": ["import shutil\nimport uuid\n\n# Let's create an example conversation a user might have with the assistant\ntutorial_questions = [\n \"Hi there, what time is my flight?\",\n \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n \"Update my flight to sometime next week then\",\n \"The next available option is great\",\n \"what about lodging and transportation?\",\n \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n \"yes go ahead and book anything that's moderate expense and has availability.\",\n \"Now for a car, what are my options?\",\n \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n \"Cool so now what recommendations do you have on excursions?\",\n \"Are they available while I'm there?\",\n \"interesting - i like the museums, what options are there? \",\n \"OK great pick one and book it for my second day there.\",\n]\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\n\n_printed = set()\nfor question in tutorial_questions:\n events = part_1_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)"] }, { "cell_type": "markdown", @@ -1818,90 +887,7 @@ "id": "c5098273-e1f6-46bf-b63b-172bbd3d9104", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.runnables import Runnable, RunnableConfig\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " user_info: str\n", - "\n", - "\n", - "class Assistant:\n", - " def __init__(self, runnable: Runnable):\n", - " self.runnable = runnable\n", - "\n", - " def __call__(self, state: State, config: RunnableConfig):\n", - " while True:\n", - " result = self.runnable.invoke(state)\n", - " # If the LLM happens to return an empty response, we will re-prompt it\n", - " # for an actual response.\n", - " if not result.tool_calls and (\n", - " not result.content\n", - " or isinstance(result.content, list)\n", - " and not result.content[0].get(\"text\")\n", - " ):\n", - " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n", - " state = {**state, \"messages\": messages}\n", - " else:\n", - " break\n", - " return {\"messages\": result}\n", - "\n", - "\n", - "# Haiku is faster and cheaper, but less accurate\n", - "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n", - "# You could also use OpenAI or another model, though you will likely have\n", - "# to adapt the prompts\n", - "# from langchain_openai import ChatOpenAI\n", - "\n", - "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", - "\n", - "assistant_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a helpful customer support assistant for Swiss Airlines. \"\n", - " \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \" If a search comes up empty, expand your search before giving up.\"\n", - " \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n", - " \"\\nCurrent time: {time}.\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "part_2_tools = [\n", - " TavilySearchResults(max_results=1),\n", - " fetch_user_flight_information,\n", - " search_flights,\n", - " lookup_policy,\n", - " update_ticket_to_new_flight,\n", - " cancel_ticket,\n", - " search_car_rentals,\n", - " book_car_rental,\n", - " update_car_rental,\n", - " cancel_car_rental,\n", - " search_hotels,\n", - " book_hotel,\n", - " update_hotel,\n", - " cancel_hotel,\n", - " search_trip_recommendations,\n", - " book_excursion,\n", - " update_excursion,\n", - " cancel_excursion,\n", - "]\n", - "part_2_assistant_runnable = assistant_prompt | llm.bind_tools(part_2_tools)" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import Runnable, RunnableConfig\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n user_info: str\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n result = self.runnable.invoke(state)\n # If the LLM happens to return an empty response, we will re-prompt it\n # for an actual response.\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\n# Haiku is faster and cheaper, but less accurate\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n# You could also use OpenAI or another model, though you will likely have\n# to adapt the prompts\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nassistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\npart_2_tools = [\n TavilySearchResults(max_results=1),\n fetch_user_flight_information,\n search_flights,\n lookup_policy,\n update_ticket_to_new_flight,\n cancel_ticket,\n search_car_rentals,\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n search_hotels,\n book_hotel,\n update_hotel,\n cancel_hotel,\n search_trip_recommendations,\n book_excursion,\n update_excursion,\n cancel_excursion,\n]\npart_2_assistant_runnable = assistant_prompt | llm.bind_tools(part_2_tools)"] }, { "cell_type": "markdown", @@ -1922,40 +908,7 @@ "id": "910002ce-2431-4280-854a-a273c517611b", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.prebuilt import tools_condition\n", - "\n", - "builder = StateGraph(State)\n", - "\n", - "\n", - "def user_info(state: State):\n", - " return {\"user_info\": fetch_user_flight_information.invoke({})}\n", - "\n", - "\n", - "# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n", - "# having to take an action\n", - "builder.add_node(\"fetch_user_info\", user_info)\n", - "builder.set_entry_point(\"fetch_user_info\")\n", - "builder.add_node(\"assistant\", Assistant(part_2_assistant_runnable))\n", - "builder.add_node(\"tools\", create_tool_node_with_fallback(part_2_tools))\n", - "builder.add_edge(\"fetch_user_info\", \"assistant\")\n", - "builder.add_conditional_edges(\n", - " \"assistant\",\n", - " tools_condition,\n", - ")\n", - "builder.add_edge(\"tools\", \"assistant\")\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "part_2_graph = builder.compile(\n", - " checkpointer=memory,\n", - " # NEW: The graph will always halt before executing the \"tools\" node.\n", - " # The user can approve or reject (or even alter the request) before\n", - " # the assistant continues\n", - " interrupt_before=[\"tools\"],\n", - ")" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\ndef user_info(state: State):\n return {\"user_info\": fetch_user_flight_information.invoke({})}\n\n\n# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n# having to take an action\nbuilder.add_node(\"fetch_user_info\", user_info)\nbuilder.add_edge(START, \"fetch_user_info\")\nbuilder.add_node(\"assistant\", Assistant(part_2_assistant_runnable))\nbuilder.add_node(\"tools\", create_tool_node_with_fallback(part_2_tools))\nbuilder.add_edge(\"fetch_user_info\", \"assistant\")\nbuilder.add_conditional_edges(\n \"assistant\",\n tools_condition,\n)\nbuilder.add_edge(\"tools\", \"assistant\")\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_2_graph = builder.compile(\n checkpointer=memory,\n # NEW: The graph will always halt before executing the \"tools\" node.\n # The user can approve or reject (or even alter the request) before\n # the assistant continues\n interrupt_before=[\"tools\"],\n)"] }, { "cell_type": "code", @@ -1974,15 +927,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(part_2_graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_2_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -2293,64 +1238,7 @@ ] } ], - "source": [ - "import shutil\n", - "import uuid\n", - "\n", - "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", - "thread_id = str(uuid.uuid4())\n", - "\n", - "config = {\n", - " \"configurable\": {\n", - " # The passenger_id is used in our flight tools to\n", - " # fetch the user's flight information\n", - " \"passenger_id\": \"3442 587242\",\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "\n", - "_printed = set()\n", - "# We can reuse the tutorial questions from part 1 to see how it does.\n", - "for question in tutorial_questions:\n", - " events = part_2_graph.stream(\n", - " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n", - " )\n", - " for event in events:\n", - " _print_event(event, _printed)\n", - " snapshot = part_2_graph.get_state(config)\n", - " while snapshot.next:\n", - " # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n", - " # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n", - " # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n", - " user_input = input(\n", - " \"Do you approve of the above actions? Type 'y' to continue;\"\n", - " \" otherwise, explain your requested changed.\\n\\n\"\n", - " )\n", - " if user_input.strip() == \"y\":\n", - " # Just continue\n", - " result = part_2_graph.invoke(\n", - " None,\n", - " config,\n", - " )\n", - " else:\n", - " # Satisfy the tool invocation by\n", - " # providing instructions on the requested changes / change of mind\n", - " result = part_2_graph.invoke(\n", - " {\n", - " \"messages\": [\n", - " ToolMessage(\n", - " tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n", - " content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n", - " )\n", - " ]\n", - " },\n", - " config,\n", - " )\n", - " snapshot = part_2_graph.get_state(config)" - ] + "source": ["import shutil\nimport uuid\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\n\n_printed = set()\n# We can reuse the tutorial questions from part 1 to see how it does.\nfor question in tutorial_questions:\n events = part_2_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)\n snapshot = part_2_graph.get_state(config)\n while snapshot.next:\n # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n user_input = input(\n \"Do you approve of the above actions? Type 'y' to continue;\"\n \" otherwise, explain your requested changed.\\n\\n\"\n )\n if user_input.strip() == \"y\":\n # Just continue\n result = part_2_graph.invoke(\n None,\n config,\n )\n else:\n # Satisfy the tool invocation by\n # providing instructions on the requested changes / change of mind\n result = part_2_graph.invoke(\n {\n \"messages\": [\n ToolMessage(\n tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n )\n ]\n },\n config,\n )\n snapshot = part_2_graph.get_state(config)"] }, { "cell_type": "markdown", @@ -2395,102 +1283,7 @@ "id": "20f99193-9195-42ae-8df1-0cf1489a164c", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.runnables import Runnable, RunnableConfig\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " user_info: str\n", - "\n", - "\n", - "class Assistant:\n", - " def __init__(self, runnable: Runnable):\n", - " self.runnable = runnable\n", - "\n", - " def __call__(self, state: State, config: RunnableConfig):\n", - " while True:\n", - " result = self.runnable.invoke(state)\n", - " # If the LLM happens to return an empty response, we will re-prompt it\n", - " # for an actual response.\n", - " if not result.tool_calls and (\n", - " not result.content\n", - " or isinstance(result.content, list)\n", - " and not result.content[0].get(\"text\")\n", - " ):\n", - " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n", - " state = {**state, \"messages\": messages}\n", - " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n", - " state = {**state, \"messages\": messages}\n", - " else:\n", - " break\n", - " return {\"messages\": result}\n", - "\n", - "\n", - "# Haiku is faster and cheaper, but less accurate\n", - "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n", - "# You can update the LLMs, though you may need to update the prompts\n", - "# from langchain_openai import ChatOpenAI\n", - "\n", - "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", - "\n", - "assistant_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a helpful customer support assistant for Swiss Airlines. \"\n", - " \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \" If a search comes up empty, expand your search before giving up.\"\n", - " \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n", - " \"\\nCurrent time: {time}.\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "\n", - "# \"Read\"-only tools (such as retrievers) don't need a user confirmation to use\n", - "part_3_safe_tools = [\n", - " TavilySearchResults(max_results=1),\n", - " fetch_user_flight_information,\n", - " search_flights,\n", - " lookup_policy,\n", - " search_car_rentals,\n", - " search_hotels,\n", - " search_trip_recommendations,\n", - "]\n", - "\n", - "# These tools all change the user's reservations.\n", - "# The user has the right to control what decisions are made\n", - "part_3_sensitive_tools = [\n", - " update_ticket_to_new_flight,\n", - " cancel_ticket,\n", - " book_car_rental,\n", - " update_car_rental,\n", - " cancel_car_rental,\n", - " book_hotel,\n", - " update_hotel,\n", - " cancel_hotel,\n", - " book_excursion,\n", - " update_excursion,\n", - " cancel_excursion,\n", - "]\n", - "sensitive_tool_names = {t.name for t in part_3_sensitive_tools}\n", - "# Our LLM doesn't have to know which nodes it has to route to. In its 'mind', it's just invoking functions.\n", - "part_3_assistant_runnable = assistant_prompt | llm.bind_tools(\n", - " part_3_safe_tools + part_3_sensitive_tools\n", - ")" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import Runnable, RunnableConfig\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n user_info: str\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n result = self.runnable.invoke(state)\n # If the LLM happens to return an empty response, we will re-prompt it\n # for an actual response.\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\n# Haiku is faster and cheaper, but less accurate\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n# You can update the LLMs, though you may need to update the prompts\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nassistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\n\n# \"Read\"-only tools (such as retrievers) don't need a user confirmation to use\npart_3_safe_tools = [\n TavilySearchResults(max_results=1),\n fetch_user_flight_information,\n search_flights,\n lookup_policy,\n search_car_rentals,\n search_hotels,\n search_trip_recommendations,\n]\n\n# These tools all change the user's reservations.\n# The user has the right to control what decisions are made\npart_3_sensitive_tools = [\n update_ticket_to_new_flight,\n cancel_ticket,\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n book_hotel,\n update_hotel,\n cancel_hotel,\n book_excursion,\n update_excursion,\n cancel_excursion,\n]\nsensitive_tool_names = {t.name for t in part_3_sensitive_tools}\n# Our LLM doesn't have to know which nodes it has to route to. In its 'mind', it's just invoking functions.\npart_3_assistant_runnable = assistant_prompt | llm.bind_tools(\n part_3_safe_tools + part_3_sensitive_tools\n)"] }, { "cell_type": "markdown", @@ -2508,63 +1301,7 @@ "id": "928b756f-2934-4b1b-95d1-0c4f974b978f", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.prebuilt import tools_condition\n", - "\n", - "builder = StateGraph(State)\n", - "\n", - "\n", - "def user_info(state: State):\n", - " return {\"user_info\": fetch_user_flight_information.invoke({})}\n", - "\n", - "\n", - "# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n", - "# having to take an action\n", - "builder.add_node(\"fetch_user_info\", user_info)\n", - "builder.set_entry_point(\"fetch_user_info\")\n", - "builder.add_node(\"assistant\", Assistant(part_3_assistant_runnable))\n", - "builder.add_node(\"safe_tools\", create_tool_node_with_fallback(part_3_safe_tools))\n", - "builder.add_node(\n", - " \"sensitive_tools\", create_tool_node_with_fallback(part_3_sensitive_tools)\n", - ")\n", - "# Define logic\n", - "builder.add_edge(\"fetch_user_info\", \"assistant\")\n", - "\n", - "\n", - "def route_tools(state: State) -> Literal[\"safe_tools\", \"sensitive_tools\", \"__end__\"]:\n", - " next_node = tools_condition(state)\n", - " # If no tools are invoked, return to the user\n", - " if next_node == END:\n", - " return END\n", - " ai_message = state[\"messages\"][-1]\n", - " # This assumes single tool calls. To handle parallel tool calling, you'd want to\n", - " # use an ANY condition\n", - " first_tool_call = ai_message.tool_calls[0]\n", - " if first_tool_call[\"name\"] in sensitive_tool_names:\n", - " return \"sensitive_tools\"\n", - " return \"safe_tools\"\n", - "\n", - "\n", - "builder.add_conditional_edges(\n", - " \"assistant\",\n", - " route_tools,\n", - ")\n", - "builder.add_edge(\"safe_tools\", \"assistant\")\n", - "builder.add_edge(\"sensitive_tools\", \"assistant\")\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "part_3_graph = builder.compile(\n", - " checkpointer=memory,\n", - " # NEW: The graph will always halt before executing the \"tools\" node.\n", - " # The user can approve or reject (or even alter the request) before\n", - " # the assistant continues\n", - " interrupt_before=[\"sensitive_tools\"],\n", - ")" - ] + "source": ["from typing import Literal\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\ndef user_info(state: State):\n return {\"user_info\": fetch_user_flight_information.invoke({})}\n\n\n# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n# having to take an action\nbuilder.add_node(\"fetch_user_info\", user_info)\nbuilder.add_edge(START, \"fetch_user_info\")\nbuilder.add_node(\"assistant\", Assistant(part_3_assistant_runnable))\nbuilder.add_node(\"safe_tools\", create_tool_node_with_fallback(part_3_safe_tools))\nbuilder.add_node(\n \"sensitive_tools\", create_tool_node_with_fallback(part_3_sensitive_tools)\n)\n# Define logic\nbuilder.add_edge(\"fetch_user_info\", \"assistant\")\n\n\ndef route_tools(state: State) -> Literal[\"safe_tools\", \"sensitive_tools\", \"__end__\"]:\n next_node = tools_condition(state)\n # If no tools are invoked, return to the user\n if next_node == END:\n return END\n ai_message = state[\"messages\"][-1]\n # This assumes single tool calls. To handle parallel tool calling, you'd want to\n # use an ANY condition\n first_tool_call = ai_message.tool_calls[0]\n if first_tool_call[\"name\"] in sensitive_tool_names:\n return \"sensitive_tools\"\n return \"safe_tools\"\n\n\nbuilder.add_conditional_edges(\n \"assistant\",\n route_tools,\n)\nbuilder.add_edge(\"safe_tools\", \"assistant\")\nbuilder.add_edge(\"sensitive_tools\", \"assistant\")\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_3_graph = builder.compile(\n checkpointer=memory,\n # NEW: The graph will always halt before executing the \"tools\" node.\n # The user can approve or reject (or even alter the request) before\n # the assistant continues\n interrupt_before=[\"sensitive_tools\"],\n)"] }, { "cell_type": "code", @@ -2583,15 +1320,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(part_3_graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_3_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -2897,81 +1626,7 @@ ] } ], - "source": [ - "import shutil\n", - "import uuid\n", - "\n", - "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", - "thread_id = str(uuid.uuid4())\n", - "\n", - "config = {\n", - " \"configurable\": {\n", - " # The passenger_id is used in our flight tools to\n", - " # fetch the user's flight information\n", - " \"passenger_id\": \"3442 587242\",\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "tutorial_questions = [\n", - " \"Hi there, what time is my flight?\",\n", - " \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n", - " \"Update my flight to sometime next week then\",\n", - " \"The next available option is great\",\n", - " \"what about lodging and transportation?\",\n", - " \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n", - " \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n", - " \"yes go ahead and book anything that's moderate expense and has availability.\",\n", - " \"Now for a car, what are my options?\",\n", - " \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n", - " \"Cool so now what recommendations do you have on excursions?\",\n", - " \"Are they available while I'm there?\",\n", - " \"interesting - i like the museums, what options are there? \",\n", - " \"OK great pick one and book it for my second day there.\",\n", - "]\n", - "\n", - "\n", - "_printed = set()\n", - "# We can reuse the tutorial questions from part 1 to see how it does.\n", - "for question in tutorial_questions:\n", - " events = part_3_graph.stream(\n", - " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n", - " )\n", - " for event in events:\n", - " _print_event(event, _printed)\n", - " snapshot = part_3_graph.get_state(config)\n", - " while snapshot.next:\n", - " # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n", - " # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n", - " # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n", - " user_input = input(\n", - " \"Do you approve of the above actions? Type 'y' to continue;\"\n", - " \" otherwise, explain your requested changed.\\n\\n\"\n", - " )\n", - " if user_input.strip() == \"y\":\n", - " # Just continue\n", - " result = part_3_graph.invoke(\n", - " None,\n", - " config,\n", - " )\n", - " else:\n", - " # Satisfy the tool invocation by\n", - " # providing instructions on the requested changes / change of mind\n", - " result = part_3_graph.invoke(\n", - " {\n", - " \"messages\": [\n", - " ToolMessage(\n", - " tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n", - " content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n", - " )\n", - " ]\n", - " },\n", - " config,\n", - " )\n", - " snapshot = part_3_graph.get_state(config)" - ] + "source": ["import shutil\nimport uuid\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\ntutorial_questions = [\n \"Hi there, what time is my flight?\",\n \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n \"Update my flight to sometime next week then\",\n \"The next available option is great\",\n \"what about lodging and transportation?\",\n \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n \"yes go ahead and book anything that's moderate expense and has availability.\",\n \"Now for a car, what are my options?\",\n \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n \"Cool so now what recommendations do you have on excursions?\",\n \"Are they available while I'm there?\",\n \"interesting - i like the museums, what options are there? \",\n \"OK great pick one and book it for my second day there.\",\n]\n\n\n_printed = set()\n# We can reuse the tutorial questions from part 1 to see how it does.\nfor question in tutorial_questions:\n events = part_3_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)\n snapshot = part_3_graph.get_state(config)\n while snapshot.next:\n # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n user_input = input(\n \"Do you approve of the above actions? Type 'y' to continue;\"\n \" otherwise, explain your requested changed.\\n\\n\"\n )\n if user_input.strip() == \"y\":\n # Just continue\n result = part_3_graph.invoke(\n None,\n config,\n )\n else:\n # Satisfy the tool invocation by\n # providing instructions on the requested changes / change of mind\n result = part_3_graph.invoke(\n {\n \"messages\": [\n ToolMessage(\n tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n )\n ]\n },\n config,\n )\n snapshot = part_3_graph.get_state(config)"] }, { "cell_type": "markdown", @@ -3017,39 +1672,7 @@ "id": "2997e1f9-3a4b-4794-b71f-992da3a644fa", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated, Literal, Optional\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:\n", - " \"\"\"Push or pop the state.\"\"\"\n", - " if right is None:\n", - " return left\n", - " if right == \"pop\":\n", - " return left[:-1]\n", - " return left + [right]\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " user_info: str\n", - " dialog_state: Annotated[\n", - " list[\n", - " Literal[\n", - " \"assistant\",\n", - " \"update_flight\",\n", - " \"book_car_rental\",\n", - " \"book_hotel\",\n", - " \"book_excursion\",\n", - " ]\n", - " ],\n", - " update_dialog_stack,\n", - " ]" - ] + "source": ["from typing import Annotated, Literal, Optional\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\ndef update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:\n \"\"\"Push or pop the state.\"\"\"\n if right is None:\n return left\n if right == \"pop\":\n return left[:-1]\n return left + [right]\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n user_info: str\n dialog_state: Annotated[\n list[\n Literal[\n \"assistant\",\n \"update_flight\",\n \"book_car_rental\",\n \"book_hotel\",\n \"book_excursion\",\n ]\n ],\n update_dialog_stack,\n ]"] }, { "cell_type": "markdown", @@ -3079,301 +1702,7 @@ "id": "1ef67c85-b999-406c-a745-09fdc0dfa0b3", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_core.runnables import Runnable, RunnableConfig\n", - "\n", - "\n", - "class Assistant:\n", - " def __init__(self, runnable: Runnable):\n", - " self.runnable = runnable\n", - "\n", - " def __call__(self, state: State, config: RunnableConfig):\n", - " while True:\n", - " result = self.runnable.invoke(state)\n", - "\n", - " if not result.tool_calls and (\n", - " not result.content\n", - " or isinstance(result.content, list)\n", - " and not result.content[0].get(\"text\")\n", - " ):\n", - " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n", - " state = {**state, \"messages\": messages}\n", - " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n", - " state = {**state, \"messages\": messages}\n", - " else:\n", - " break\n", - " return {\"messages\": result}\n", - "\n", - "\n", - "class CompleteOrEscalate(BaseModel):\n", - " \"\"\"A tool to mark the current task as completed and/or to escalate control of the dialog to the main assistant,\n", - " who can re-route the dialog based on the user's needs.\"\"\"\n", - "\n", - " cancel: bool = True\n", - " reason: str\n", - "\n", - " class Config:\n", - " schema_extra = {\n", - " \"example\": {\n", - " \"cancel\": True,\n", - " \"reason\": \"User changed their mind about the current task.\",\n", - " },\n", - " \"example 2\": {\n", - " \"cancel\": True,\n", - " \"reason\": \"I have fully completed the task.\",\n", - " },\n", - " \"example 3\": {\n", - " \"cancel\": False,\n", - " \"reason\": \"I need to search the user's emails or calendar for more information.\",\n", - " },\n", - " }\n", - "\n", - "\n", - "# Flight booking assistant\n", - "\n", - "flight_booking_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a specialized assistant for handling flight updates. \"\n", - " \" The primary assistant delegates work to you whenever the user needs help updating their bookings. \"\n", - " \"Confirm the updated flight details with the customer and inform them of any additional fees. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n", - " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n", - " \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n", - " \"\\nCurrent time: {time}.\"\n", - " \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then\"\n", - " ' \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.',\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "update_flight_safe_tools = [search_flights]\n", - "update_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]\n", - "update_flight_tools = update_flight_safe_tools + update_flight_sensitive_tools\n", - "update_flight_runnable = flight_booking_prompt | llm.bind_tools(\n", - " update_flight_tools + [CompleteOrEscalate]\n", - ")\n", - "\n", - "# Hotel Booking Assistant\n", - "book_hotel_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a specialized assistant for handling hotel bookings. \"\n", - " \"The primary assistant delegates work to you whenever the user needs help booking a hotel. \"\n", - " \"Search for available hotels based on the user's preferences and confirm the booking details with the customer. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n", - " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n", - " \"\\nCurrent time: {time}.\"\n", - " '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant.'\n", - " \" Do not waste the user's time. Do not make up invalid tools or functions.\"\n", - " \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n", - " \" - 'what's the weather like this time of year?'\\n\"\n", - " \" - 'nevermind i think I'll book separately'\\n\"\n", - " \" - 'i need to figure out transportation while i'm there'\\n\"\n", - " \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n", - " \" - 'Hotel booking confirmed'\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "book_hotel_safe_tools = [search_hotels]\n", - "book_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]\n", - "book_hotel_tools = book_hotel_safe_tools + book_hotel_sensitive_tools\n", - "book_hotel_runnable = book_hotel_prompt | llm.bind_tools(\n", - " book_hotel_tools + [CompleteOrEscalate]\n", - ")\n", - "\n", - "# Car Rental Assistant\n", - "book_car_rental_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a specialized assistant for handling car rental bookings. \"\n", - " \"The primary assistant delegates work to you whenever the user needs help booking a car rental. \"\n", - " \"Search for available car rentals based on the user's preferences and confirm the booking details with the customer. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n", - " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n", - " \"\\nCurrent time: {time}.\"\n", - " \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"\n", - " '\"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n", - " \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n", - " \" - 'what's the weather like this time of year?'\\n\"\n", - " \" - 'What flights are available?'\\n\"\n", - " \" - 'nevermind i think I'll book separately'\\n\"\n", - " \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n", - " \" - 'Car rental booking confirmed'\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "book_car_rental_safe_tools = [search_car_rentals]\n", - "book_car_rental_sensitive_tools = [\n", - " book_car_rental,\n", - " update_car_rental,\n", - " cancel_car_rental,\n", - "]\n", - "book_car_rental_tools = book_car_rental_safe_tools + book_car_rental_sensitive_tools\n", - "book_car_rental_runnable = book_car_rental_prompt | llm.bind_tools(\n", - " book_car_rental_tools + [CompleteOrEscalate]\n", - ")\n", - "\n", - "# Excursion Assistant\n", - "\n", - "book_excursion_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a specialized assistant for handling trip recommendations. \"\n", - " \"The primary assistant delegates work to you whenever the user needs help booking a recommended trip. \"\n", - " \"Search for available trip recommendations based on the user's preferences and confirm the booking details with the customer. \"\n", - " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n", - " \"\\nCurrent time: {time}.\"\n", - " '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n", - " \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n", - " \" - 'nevermind i think I'll book separately'\\n\"\n", - " \" - 'i need to figure out transportation while i'm there'\\n\"\n", - " \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n", - " \" - 'Excursion booking confirmed!'\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "\n", - "book_excursion_safe_tools = [search_trip_recommendations]\n", - "book_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]\n", - "book_excursion_tools = book_excursion_safe_tools + book_excursion_sensitive_tools\n", - "book_excursion_runnable = book_excursion_prompt | llm.bind_tools(\n", - " book_excursion_tools + [CompleteOrEscalate]\n", - ")\n", - "\n", - "\n", - "# Primary Assistant\n", - "class ToFlightBookingAssistant(BaseModel):\n", - " \"\"\"Transfers work to a specialized assistant to handle flight updates and cancellations.\"\"\"\n", - "\n", - " request: str = Field(\n", - " description=\"Any necessary followup questions the update flight assistant should clarify before proceeding.\"\n", - " )\n", - "\n", - "\n", - "class ToBookCarRental(BaseModel):\n", - " \"\"\"Transfers work to a specialized assistant to handle car rental bookings.\"\"\"\n", - "\n", - " location: str = Field(\n", - " description=\"The location where the user wants to rent a car.\"\n", - " )\n", - " start_date: str = Field(description=\"The start date of the car rental.\")\n", - " end_date: str = Field(description=\"The end date of the car rental.\")\n", - " request: str = Field(\n", - " description=\"Any additional information or requests from the user regarding the car rental.\"\n", - " )\n", - "\n", - " class Config:\n", - " schema_extra = {\n", - " \"example\": {\n", - " \"location\": \"Basel\",\n", - " \"start_date\": \"2023-07-01\",\n", - " \"end_date\": \"2023-07-05\",\n", - " \"request\": \"I need a compact car with automatic transmission.\",\n", - " }\n", - " }\n", - "\n", - "\n", - "class ToHotelBookingAssistant(BaseModel):\n", - " \"\"\"Transfer work to a specialized assistant to handle hotel bookings.\"\"\"\n", - "\n", - " location: str = Field(\n", - " description=\"The location where the user wants to book a hotel.\"\n", - " )\n", - " checkin_date: str = Field(description=\"The check-in date for the hotel.\")\n", - " checkout_date: str = Field(description=\"The check-out date for the hotel.\")\n", - " request: str = Field(\n", - " description=\"Any additional information or requests from the user regarding the hotel booking.\"\n", - " )\n", - "\n", - " class Config:\n", - " schema_extra = {\n", - " \"example\": {\n", - " \"location\": \"Zurich\",\n", - " \"checkin_date\": \"2023-08-15\",\n", - " \"checkout_date\": \"2023-08-20\",\n", - " \"request\": \"I prefer a hotel near the city center with a room that has a view.\",\n", - " }\n", - " }\n", - "\n", - "\n", - "class ToBookExcursion(BaseModel):\n", - " \"\"\"Transfers work to a specialized assistant to handle trip recommendation and other excursion bookings.\"\"\"\n", - "\n", - " location: str = Field(\n", - " description=\"The location where the user wants to book a recommended trip.\"\n", - " )\n", - " request: str = Field(\n", - " description=\"Any additional information or requests from the user regarding the trip recommendation.\"\n", - " )\n", - "\n", - " class Config:\n", - " schema_extra = {\n", - " \"example\": {\n", - " \"location\": \"Lucerne\",\n", - " \"request\": \"The user is interested in outdoor activities and scenic views.\",\n", - " }\n", - " }\n", - "\n", - "\n", - "# The top-level assistant performs general Q&A and delegates specialized tasks to other assistants.\n", - "# The task delegation is a simple form of semantic routing / does simple intent detection\n", - "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n", - "\n", - "primary_assistant_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a helpful customer support assistant for Swiss Airlines. \"\n", - " \"Your primary role is to search for flight information and company policies to answer customer queries. \"\n", - " \"If a customer requests to update or cancel a flight, book a car rental, book a hotel, or get trip recommendations, \"\n", - " \"delegate the task to the appropriate specialized assistant by invoking the corresponding tool. You are not able to make these types of changes yourself.\"\n", - " \" Only the specialized assistants are given permission to do this for the user.\"\n", - " \"The user is not aware of the different specialized assistants, so do not mention them; just quietly delegate through function calls. \"\n", - " \"Provide detailed information to the customer, and always double-check the database before concluding that information is unavailable. \"\n", - " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n", - " \" If a search comes up empty, expand your search before giving up.\"\n", - " \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n", - " \"\\nCurrent time: {time}.\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ").partial(time=datetime.now())\n", - "primary_assistant_tools = [\n", - " TavilySearchResults(max_results=1),\n", - " search_flights,\n", - " lookup_policy,\n", - "]\n", - "assistant_runnable = primary_assistant_prompt | llm.bind_tools(\n", - " primary_assistant_tools\n", - " + [\n", - " ToFlightBookingAssistant,\n", - " ToBookCarRental,\n", - " ToHotelBookingAssistant,\n", - " ToBookExcursion,\n", - " ]\n", - ")" - ] + "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_core.runnables import Runnable, RunnableConfig\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n result = self.runnable.invoke(state)\n\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\nclass CompleteOrEscalate(BaseModel):\n \"\"\"A tool to mark the current task as completed and/or to escalate control of the dialog to the main assistant,\n who can re-route the dialog based on the user's needs.\"\"\"\n\n cancel: bool = True\n reason: str\n\n class Config:\n schema_extra = {\n \"example\": {\n \"cancel\": True,\n \"reason\": \"User changed their mind about the current task.\",\n },\n \"example 2\": {\n \"cancel\": True,\n \"reason\": \"I have fully completed the task.\",\n },\n \"example 3\": {\n \"cancel\": False,\n \"reason\": \"I need to search the user's emails or calendar for more information.\",\n },\n }\n\n\n# Flight booking assistant\n\nflight_booking_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling flight updates. \"\n \" The primary assistant delegates work to you whenever the user needs help updating their bookings. \"\n \"Confirm the updated flight details with the customer and inform them of any additional fees. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\"\n \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then\"\n ' \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.',\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nupdate_flight_safe_tools = [search_flights]\nupdate_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]\nupdate_flight_tools = update_flight_safe_tools + update_flight_sensitive_tools\nupdate_flight_runnable = flight_booking_prompt | llm.bind_tools(\n update_flight_tools + [CompleteOrEscalate]\n)\n\n# Hotel Booking Assistant\nbook_hotel_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling hotel bookings. \"\n \"The primary assistant delegates work to you whenever the user needs help booking a hotel. \"\n \"Search for available hotels based on the user's preferences and confirm the booking details with the customer. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\nCurrent time: {time}.\"\n '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant.'\n \" Do not waste the user's time. Do not make up invalid tools or functions.\"\n \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n \" - 'what's the weather like this time of year?'\\n\"\n \" - 'nevermind i think I'll book separately'\\n\"\n \" - 'i need to figure out transportation while i'm there'\\n\"\n \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n \" - 'Hotel booking confirmed'\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nbook_hotel_safe_tools = [search_hotels]\nbook_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]\nbook_hotel_tools = book_hotel_safe_tools + book_hotel_sensitive_tools\nbook_hotel_runnable = book_hotel_prompt | llm.bind_tools(\n book_hotel_tools + [CompleteOrEscalate]\n)\n\n# Car Rental Assistant\nbook_car_rental_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling car rental bookings. \"\n \"The primary assistant delegates work to you whenever the user needs help booking a car rental. \"\n \"Search for available car rentals based on the user's preferences and confirm the booking details with the customer. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\nCurrent time: {time}.\"\n \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"\n '\"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n \" - 'what's the weather like this time of year?'\\n\"\n \" - 'What flights are available?'\\n\"\n \" - 'nevermind i think I'll book separately'\\n\"\n \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n \" - 'Car rental booking confirmed'\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nbook_car_rental_safe_tools = [search_car_rentals]\nbook_car_rental_sensitive_tools = [\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n]\nbook_car_rental_tools = book_car_rental_safe_tools + book_car_rental_sensitive_tools\nbook_car_rental_runnable = book_car_rental_prompt | llm.bind_tools(\n book_car_rental_tools + [CompleteOrEscalate]\n)\n\n# Excursion Assistant\n\nbook_excursion_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling trip recommendations. \"\n \"The primary assistant delegates work to you whenever the user needs help booking a recommended trip. \"\n \"Search for available trip recommendations based on the user's preferences and confirm the booking details with the customer. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\nCurrent time: {time}.\"\n '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n \" - 'nevermind i think I'll book separately'\\n\"\n \" - 'i need to figure out transportation while i'm there'\\n\"\n \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n \" - 'Excursion booking confirmed!'\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nbook_excursion_safe_tools = [search_trip_recommendations]\nbook_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]\nbook_excursion_tools = book_excursion_safe_tools + book_excursion_sensitive_tools\nbook_excursion_runnable = book_excursion_prompt | llm.bind_tools(\n book_excursion_tools + [CompleteOrEscalate]\n)\n\n\n# Primary Assistant\nclass ToFlightBookingAssistant(BaseModel):\n \"\"\"Transfers work to a specialized assistant to handle flight updates and cancellations.\"\"\"\n\n request: str = Field(\n description=\"Any necessary followup questions the update flight assistant should clarify before proceeding.\"\n )\n\n\nclass ToBookCarRental(BaseModel):\n \"\"\"Transfers work to a specialized assistant to handle car rental bookings.\"\"\"\n\n location: str = Field(\n description=\"The location where the user wants to rent a car.\"\n )\n start_date: str = Field(description=\"The start date of the car rental.\")\n end_date: str = Field(description=\"The end date of the car rental.\")\n request: str = Field(\n description=\"Any additional information or requests from the user regarding the car rental.\"\n )\n\n class Config:\n schema_extra = {\n \"example\": {\n \"location\": \"Basel\",\n \"start_date\": \"2023-07-01\",\n \"end_date\": \"2023-07-05\",\n \"request\": \"I need a compact car with automatic transmission.\",\n }\n }\n\n\nclass ToHotelBookingAssistant(BaseModel):\n \"\"\"Transfer work to a specialized assistant to handle hotel bookings.\"\"\"\n\n location: str = Field(\n description=\"The location where the user wants to book a hotel.\"\n )\n checkin_date: str = Field(description=\"The check-in date for the hotel.\")\n checkout_date: str = Field(description=\"The check-out date for the hotel.\")\n request: str = Field(\n description=\"Any additional information or requests from the user regarding the hotel booking.\"\n )\n\n class Config:\n schema_extra = {\n \"example\": {\n \"location\": \"Zurich\",\n \"checkin_date\": \"2023-08-15\",\n \"checkout_date\": \"2023-08-20\",\n \"request\": \"I prefer a hotel near the city center with a room that has a view.\",\n }\n }\n\n\nclass ToBookExcursion(BaseModel):\n \"\"\"Transfers work to a specialized assistant to handle trip recommendation and other excursion bookings.\"\"\"\n\n location: str = Field(\n description=\"The location where the user wants to book a recommended trip.\"\n )\n request: str = Field(\n description=\"Any additional information or requests from the user regarding the trip recommendation.\"\n )\n\n class Config:\n schema_extra = {\n \"example\": {\n \"location\": \"Lucerne\",\n \"request\": \"The user is interested in outdoor activities and scenic views.\",\n }\n }\n\n\n# The top-level assistant performs general Q&A and delegates specialized tasks to other assistants.\n# The task delegation is a simple form of semantic routing / does simple intent detection\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n\nprimary_assistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \"Your primary role is to search for flight information and company policies to answer customer queries. \"\n \"If a customer requests to update or cancel a flight, book a car rental, book a hotel, or get trip recommendations, \"\n \"delegate the task to the appropriate specialized assistant by invoking the corresponding tool. You are not able to make these types of changes yourself.\"\n \" Only the specialized assistants are given permission to do this for the user.\"\n \"The user is not aware of the different specialized assistants, so do not mention them; just quietly delegate through function calls. \"\n \"Provide detailed information to the customer, and always double-check the database before concluding that information is unavailable. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\nprimary_assistant_tools = [\n TavilySearchResults(max_results=1),\n search_flights,\n lookup_policy,\n]\nassistant_runnable = primary_assistant_prompt | llm.bind_tools(\n primary_assistant_tools\n + [\n ToFlightBookingAssistant,\n ToBookCarRental,\n ToHotelBookingAssistant,\n ToBookExcursion,\n ]\n)"] }, { "cell_type": "markdown", @@ -3396,31 +1725,7 @@ "id": "fb812818-99c9-4bf3-b1e5-a394c7b9058d", "metadata": {}, "outputs": [], - "source": [ - "from typing import Callable\n", - "\n", - "from langchain_core.messages import ToolMessage\n", - "\n", - "\n", - "def create_entry_node(assistant_name: str, new_dialog_state: str) -> Callable:\n", - " def entry_node(state: State) -> dict:\n", - " tool_call_id = state[\"messages\"][-1].tool_calls[0][\"id\"]\n", - " return {\n", - " \"messages\": [\n", - " ToolMessage(\n", - " content=f\"The assistant is now the {assistant_name}. Reflect on the above conversation between the host assistant and the user.\"\n", - " f\" The user's intent is unsatisfied. Use the provided tools to assist the user. Remember, you are {assistant_name},\"\n", - " \" and the booking, update, other other action is not complete until after you have successfully invoked the appropriate tool.\"\n", - " \" If the user changes their mind or needs help for other tasks, call the CompleteOrEscalate function to let the primary host assistant take control.\"\n", - " \" Do not mention who you are - just act as the proxy for the assistant.\",\n", - " tool_call_id=tool_call_id,\n", - " )\n", - " ],\n", - " \"dialog_state\": new_dialog_state,\n", - " }\n", - "\n", - " return entry_node" - ] + "source": ["from typing import Callable\n\nfrom langchain_core.messages import ToolMessage\n\n\ndef create_entry_node(assistant_name: str, new_dialog_state: str) -> Callable:\n def entry_node(state: State) -> dict:\n tool_call_id = state[\"messages\"][-1].tool_calls[0][\"id\"]\n return {\n \"messages\": [\n ToolMessage(\n content=f\"The assistant is now the {assistant_name}. Reflect on the above conversation between the host assistant and the user.\"\n f\" The user's intent is unsatisfied. Use the provided tools to assist the user. Remember, you are {assistant_name},\"\n \" and the booking, update, other other action is not complete until after you have successfully invoked the appropriate tool.\"\n \" If the user changes their mind or needs help for other tasks, call the CompleteOrEscalate function to let the primary host assistant take control.\"\n \" Do not mention who you are - just act as the proxy for the assistant.\",\n tool_call_id=tool_call_id,\n )\n ],\n \"dialog_state\": new_dialog_state,\n }\n\n return entry_node"] }, { "cell_type": "markdown", @@ -3438,23 +1743,7 @@ "id": "b7c1140c-cd4e-4d69-bddd-7baa1eb4540e", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.prebuilt import tools_condition\n", - "\n", - "builder = StateGraph(State)\n", - "\n", - "\n", - "def user_info(state: State):\n", - " return {\"user_info\": fetch_user_flight_information.invoke({})}\n", - "\n", - "\n", - "builder.add_node(\"fetch_user_info\", user_info)\n", - "builder.set_entry_point(\"fetch_user_info\")" - ] + "source": ["from typing import Literal\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\ndef user_info(state: State):\n return {\"user_info\": fetch_user_flight_information.invoke({})}\n\n\nbuilder.add_node(\"fetch_user_info\", user_info)\nbuilder.add_edge(START, \"fetch_user_info\")"] }, { "cell_type": "markdown", @@ -3480,75 +1769,7 @@ "id": "54297dc5-80b2-4bc6-8087-803caf1e0cf7", "metadata": {}, "outputs": [], - "source": [ - "# Flight booking assistant\n", - "builder.add_node(\n", - " \"enter_update_flight\",\n", - " create_entry_node(\"Flight Updates & Booking Assistant\", \"update_flight\"),\n", - ")\n", - "builder.add_node(\"update_flight\", Assistant(update_flight_runnable))\n", - "builder.add_edge(\"enter_update_flight\", \"update_flight\")\n", - "builder.add_node(\n", - " \"update_flight_sensitive_tools\",\n", - " create_tool_node_with_fallback(update_flight_sensitive_tools),\n", - ")\n", - "builder.add_node(\n", - " \"update_flight_safe_tools\",\n", - " create_tool_node_with_fallback(update_flight_safe_tools),\n", - ")\n", - "\n", - "\n", - "def route_update_flight(\n", - " state: State,\n", - ") -> Literal[\n", - " \"update_flight_sensitive_tools\",\n", - " \"update_flight_safe_tools\",\n", - " \"leave_skill\",\n", - " \"__end__\",\n", - "]:\n", - " route = tools_condition(state)\n", - " if route == END:\n", - " return END\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n", - " if did_cancel:\n", - " return \"leave_skill\"\n", - " safe_toolnames = [t.name for t in update_flight_safe_tools]\n", - " if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n", - " return \"update_flight_safe_tools\"\n", - " return \"update_flight_sensitive_tools\"\n", - "\n", - "\n", - "builder.add_edge(\"update_flight_sensitive_tools\", \"update_flight\")\n", - "builder.add_edge(\"update_flight_safe_tools\", \"update_flight\")\n", - "builder.add_conditional_edges(\"update_flight\", route_update_flight)\n", - "\n", - "\n", - "# This node will be shared for exiting all specialized assistants\n", - "def pop_dialog_state(state: State) -> dict:\n", - " \"\"\"Pop the dialog stack and return to the main assistant.\n", - "\n", - " This lets the full graph explicitly track the dialog flow and delegate control\n", - " to specific sub-graphs.\n", - " \"\"\"\n", - " messages = []\n", - " if state[\"messages\"][-1].tool_calls:\n", - " # Note: Doesn't currently handle the edge case where the llm performs parallel tool calls\n", - " messages.append(\n", - " ToolMessage(\n", - " content=\"Resuming dialog with the host assistant. Please reflect on the past conversation and assist the user as needed.\",\n", - " tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n", - " )\n", - " )\n", - " return {\n", - " \"dialog_state\": \"pop\",\n", - " \"messages\": messages,\n", - " }\n", - "\n", - "\n", - "builder.add_node(\"leave_skill\", pop_dialog_state)\n", - "builder.add_edge(\"leave_skill\", \"primary_assistant\")" - ] + "source": ["# Flight booking assistant\nbuilder.add_node(\n \"enter_update_flight\",\n create_entry_node(\"Flight Updates & Booking Assistant\", \"update_flight\"),\n)\nbuilder.add_node(\"update_flight\", Assistant(update_flight_runnable))\nbuilder.add_edge(\"enter_update_flight\", \"update_flight\")\nbuilder.add_node(\n \"update_flight_sensitive_tools\",\n create_tool_node_with_fallback(update_flight_sensitive_tools),\n)\nbuilder.add_node(\n \"update_flight_safe_tools\",\n create_tool_node_with_fallback(update_flight_safe_tools),\n)\n\n\ndef route_update_flight(\n state: State,\n) -> Literal[\n \"update_flight_sensitive_tools\",\n \"update_flight_safe_tools\",\n \"leave_skill\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n safe_toolnames = [t.name for t in update_flight_safe_tools]\n if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n return \"update_flight_safe_tools\"\n return \"update_flight_sensitive_tools\"\n\n\nbuilder.add_edge(\"update_flight_sensitive_tools\", \"update_flight\")\nbuilder.add_edge(\"update_flight_safe_tools\", \"update_flight\")\nbuilder.add_conditional_edges(\"update_flight\", route_update_flight)\n\n\n# This node will be shared for exiting all specialized assistants\ndef pop_dialog_state(state: State) -> dict:\n \"\"\"Pop the dialog stack and return to the main assistant.\n\n This lets the full graph explicitly track the dialog flow and delegate control\n to specific sub-graphs.\n \"\"\"\n messages = []\n if state[\"messages\"][-1].tool_calls:\n # Note: Doesn't currently handle the edge case where the llm performs parallel tool calls\n messages.append(\n ToolMessage(\n content=\"Resuming dialog with the host assistant. Please reflect on the past conversation and assist the user as needed.\",\n tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n )\n )\n return {\n \"dialog_state\": \"pop\",\n \"messages\": messages,\n }\n\n\nbuilder.add_node(\"leave_skill\", pop_dialog_state)\nbuilder.add_edge(\"leave_skill\", \"primary_assistant\")"] }, { "cell_type": "markdown", @@ -3564,50 +1785,7 @@ "id": "e68b93f5-0f72-4e94-8e8b-b501ec82edcf", "metadata": {}, "outputs": [], - "source": [ - "# Car rental assistant\n", - "\n", - "builder.add_node(\n", - " \"enter_book_car_rental\",\n", - " create_entry_node(\"Car Rental Assistant\", \"book_car_rental\"),\n", - ")\n", - "builder.add_node(\"book_car_rental\", Assistant(book_car_rental_runnable))\n", - "builder.add_edge(\"enter_book_car_rental\", \"book_car_rental\")\n", - "builder.add_node(\n", - " \"book_car_rental_safe_tools\",\n", - " create_tool_node_with_fallback(book_car_rental_safe_tools),\n", - ")\n", - "builder.add_node(\n", - " \"book_car_rental_sensitive_tools\",\n", - " create_tool_node_with_fallback(book_car_rental_sensitive_tools),\n", - ")\n", - "\n", - "\n", - "def route_book_car_rental(\n", - " state: State,\n", - ") -> Literal[\n", - " \"book_car_rental_safe_tools\",\n", - " \"book_car_rental_sensitive_tools\",\n", - " \"leave_skill\",\n", - " \"__end__\",\n", - "]:\n", - " route = tools_condition(state)\n", - " if route == END:\n", - " return END\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n", - " if did_cancel:\n", - " return \"leave_skill\"\n", - " safe_toolnames = [t.name for t in book_car_rental_safe_tools]\n", - " if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n", - " return \"book_car_rental_safe_tools\"\n", - " return \"book_car_rental_sensitive_tools\"\n", - "\n", - "\n", - "builder.add_edge(\"book_car_rental_sensitive_tools\", \"book_car_rental\")\n", - "builder.add_edge(\"book_car_rental_safe_tools\", \"book_car_rental\")\n", - "builder.add_conditional_edges(\"book_car_rental\", route_book_car_rental)" - ] + "source": ["# Car rental assistant\n\nbuilder.add_node(\n \"enter_book_car_rental\",\n create_entry_node(\"Car Rental Assistant\", \"book_car_rental\"),\n)\nbuilder.add_node(\"book_car_rental\", Assistant(book_car_rental_runnable))\nbuilder.add_edge(\"enter_book_car_rental\", \"book_car_rental\")\nbuilder.add_node(\n \"book_car_rental_safe_tools\",\n create_tool_node_with_fallback(book_car_rental_safe_tools),\n)\nbuilder.add_node(\n \"book_car_rental_sensitive_tools\",\n create_tool_node_with_fallback(book_car_rental_sensitive_tools),\n)\n\n\ndef route_book_car_rental(\n state: State,\n) -> Literal[\n \"book_car_rental_safe_tools\",\n \"book_car_rental_sensitive_tools\",\n \"leave_skill\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n safe_toolnames = [t.name for t in book_car_rental_safe_tools]\n if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n return \"book_car_rental_safe_tools\"\n return \"book_car_rental_sensitive_tools\"\n\n\nbuilder.add_edge(\"book_car_rental_sensitive_tools\", \"book_car_rental\")\nbuilder.add_edge(\"book_car_rental_safe_tools\", \"book_car_rental\")\nbuilder.add_conditional_edges(\"book_car_rental\", route_book_car_rental)"] }, { "cell_type": "markdown", @@ -3623,45 +1801,7 @@ "id": "ec40edb9-d415-4f43-8f9f-c82a239c607f", "metadata": {}, "outputs": [], - "source": [ - "# Hotel booking assistant\n", - "builder.add_node(\n", - " \"enter_book_hotel\", create_entry_node(\"Hotel Booking Assistant\", \"book_hotel\")\n", - ")\n", - "builder.add_node(\"book_hotel\", Assistant(book_hotel_runnable))\n", - "builder.add_edge(\"enter_book_hotel\", \"book_hotel\")\n", - "builder.add_node(\n", - " \"book_hotel_safe_tools\",\n", - " create_tool_node_with_fallback(book_hotel_safe_tools),\n", - ")\n", - "builder.add_node(\n", - " \"book_hotel_sensitive_tools\",\n", - " create_tool_node_with_fallback(book_hotel_sensitive_tools),\n", - ")\n", - "\n", - "\n", - "def route_book_hotel(\n", - " state: State,\n", - ") -> Literal[\n", - " \"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", \"__end__\"\n", - "]:\n", - " route = tools_condition(state)\n", - " if route == END:\n", - " return END\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n", - " if did_cancel:\n", - " return \"leave_skill\"\n", - " tool_names = [t.name for t in book_hotel_safe_tools]\n", - " if all(tc[\"name\"] in tool_names for tc in tool_calls):\n", - " return \"book_hotel_safe_tools\"\n", - " return \"book_hotel_sensitive_tools\"\n", - "\n", - "\n", - "builder.add_edge(\"book_hotel_sensitive_tools\", \"book_hotel\")\n", - "builder.add_edge(\"book_hotel_safe_tools\", \"book_hotel\")\n", - "builder.add_conditional_edges(\"book_hotel\", route_book_hotel)" - ] + "source": ["# Hotel booking assistant\nbuilder.add_node(\n \"enter_book_hotel\", create_entry_node(\"Hotel Booking Assistant\", \"book_hotel\")\n)\nbuilder.add_node(\"book_hotel\", Assistant(book_hotel_runnable))\nbuilder.add_edge(\"enter_book_hotel\", \"book_hotel\")\nbuilder.add_node(\n \"book_hotel_safe_tools\",\n create_tool_node_with_fallback(book_hotel_safe_tools),\n)\nbuilder.add_node(\n \"book_hotel_sensitive_tools\",\n create_tool_node_with_fallback(book_hotel_sensitive_tools),\n)\n\n\ndef route_book_hotel(\n state: State,\n) -> Literal[\n \"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", \"__end__\"\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n tool_names = [t.name for t in book_hotel_safe_tools]\n if all(tc[\"name\"] in tool_names for tc in tool_calls):\n return \"book_hotel_safe_tools\"\n return \"book_hotel_sensitive_tools\"\n\n\nbuilder.add_edge(\"book_hotel_sensitive_tools\", \"book_hotel\")\nbuilder.add_edge(\"book_hotel_safe_tools\", \"book_hotel\")\nbuilder.add_conditional_edges(\"book_hotel\", route_book_hotel)"] }, { "cell_type": "markdown", @@ -3677,49 +1817,7 @@ "id": "2ce9cf21-f708-4033-bca6-5f5d110b5662", "metadata": {}, "outputs": [], - "source": [ - "# Excursion assistant\n", - "builder.add_node(\n", - " \"enter_book_excursion\",\n", - " create_entry_node(\"Trip Recommendation Assistant\", \"book_excursion\"),\n", - ")\n", - "builder.add_node(\"book_excursion\", Assistant(book_excursion_runnable))\n", - "builder.add_edge(\"enter_book_excursion\", \"book_excursion\")\n", - "builder.add_node(\n", - " \"book_excursion_safe_tools\",\n", - " create_tool_node_with_fallback(book_excursion_safe_tools),\n", - ")\n", - "builder.add_node(\n", - " \"book_excursion_sensitive_tools\",\n", - " create_tool_node_with_fallback(book_excursion_sensitive_tools),\n", - ")\n", - "\n", - "\n", - "def route_book_excursion(\n", - " state: State,\n", - ") -> Literal[\n", - " \"book_excursion_safe_tools\",\n", - " \"book_excursion_sensitive_tools\",\n", - " \"leave_skill\",\n", - " \"__end__\",\n", - "]:\n", - " route = tools_condition(state)\n", - " if route == END:\n", - " return END\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n", - " if did_cancel:\n", - " return \"leave_skill\"\n", - " tool_names = [t.name for t in book_excursion_safe_tools]\n", - " if all(tc[\"name\"] in tool_names for tc in tool_calls):\n", - " return \"book_excursion_safe_tools\"\n", - " return \"book_excursion_sensitive_tools\"\n", - "\n", - "\n", - "builder.add_edge(\"book_excursion_sensitive_tools\", \"book_excursion\")\n", - "builder.add_edge(\"book_excursion_safe_tools\", \"book_excursion\")\n", - "builder.add_conditional_edges(\"book_excursion\", route_book_excursion)" - ] + "source": ["# Excursion assistant\nbuilder.add_node(\n \"enter_book_excursion\",\n create_entry_node(\"Trip Recommendation Assistant\", \"book_excursion\"),\n)\nbuilder.add_node(\"book_excursion\", Assistant(book_excursion_runnable))\nbuilder.add_edge(\"enter_book_excursion\", \"book_excursion\")\nbuilder.add_node(\n \"book_excursion_safe_tools\",\n create_tool_node_with_fallback(book_excursion_safe_tools),\n)\nbuilder.add_node(\n \"book_excursion_sensitive_tools\",\n create_tool_node_with_fallback(book_excursion_sensitive_tools),\n)\n\n\ndef route_book_excursion(\n state: State,\n) -> Literal[\n \"book_excursion_safe_tools\",\n \"book_excursion_sensitive_tools\",\n \"leave_skill\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n tool_names = [t.name for t in book_excursion_safe_tools]\n if all(tc[\"name\"] in tool_names for tc in tool_calls):\n return \"book_excursion_safe_tools\"\n return \"book_excursion_sensitive_tools\"\n\n\nbuilder.add_edge(\"book_excursion_sensitive_tools\", \"book_excursion\")\nbuilder.add_edge(\"book_excursion_safe_tools\", \"book_excursion\")\nbuilder.add_conditional_edges(\"book_excursion\", route_book_excursion)"] }, { "cell_type": "markdown", @@ -3735,90 +1833,7 @@ "id": "acb19faf-66c8-4fd8-89ec-4d97d510ce4d", "metadata": {}, "outputs": [], - "source": [ - "# Primary assistant\n", - "builder.add_node(\"primary_assistant\", Assistant(assistant_runnable))\n", - "builder.add_node(\n", - " \"primary_assistant_tools\", create_tool_node_with_fallback(primary_assistant_tools)\n", - ")\n", - "\n", - "\n", - "def route_primary_assistant(\n", - " state: State,\n", - ") -> Literal[\n", - " \"primary_assistant_tools\",\n", - " \"enter_update_flight\",\n", - " \"enter_book_hotel\",\n", - " \"enter_book_excursion\",\n", - " \"__end__\",\n", - "]:\n", - " route = tools_condition(state)\n", - " if route == END:\n", - " return END\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " if tool_calls:\n", - " if tool_calls[0][\"name\"] == ToFlightBookingAssistant.__name__:\n", - " return \"enter_update_flight\"\n", - " elif tool_calls[0][\"name\"] == ToBookCarRental.__name__:\n", - " return \"enter_book_car_rental\"\n", - " elif tool_calls[0][\"name\"] == ToHotelBookingAssistant.__name__:\n", - " return \"enter_book_hotel\"\n", - " elif tool_calls[0][\"name\"] == ToBookExcursion.__name__:\n", - " return \"enter_book_excursion\"\n", - " return \"primary_assistant_tools\"\n", - " raise ValueError(\"Invalid route\")\n", - "\n", - "\n", - "# The assistant can route to one of the delegated assistants,\n", - "# directly use a tool, or directly respond to the user\n", - "builder.add_conditional_edges(\n", - " \"primary_assistant\",\n", - " route_primary_assistant,\n", - " {\n", - " \"enter_update_flight\": \"enter_update_flight\",\n", - " \"enter_book_car_rental\": \"enter_book_car_rental\",\n", - " \"enter_book_hotel\": \"enter_book_hotel\",\n", - " \"enter_book_excursion\": \"enter_book_excursion\",\n", - " \"primary_assistant_tools\": \"primary_assistant_tools\",\n", - " END: END,\n", - " },\n", - ")\n", - "builder.add_edge(\"primary_assistant_tools\", \"primary_assistant\")\n", - "\n", - "\n", - "# Each delegated workflow can directly respond to the user\n", - "# When the user responds, we want to return to the currently active workflow\n", - "def route_to_workflow(\n", - " state: State,\n", - ") -> Literal[\n", - " \"primary_assistant\",\n", - " \"update_flight\",\n", - " \"book_car_rental\",\n", - " \"book_hotel\",\n", - " \"book_excursion\",\n", - "]:\n", - " \"\"\"If we are in a delegated state, route directly to the appropriate assistant.\"\"\"\n", - " dialog_state = state.get(\"dialog_state\")\n", - " if not dialog_state:\n", - " return \"primary_assistant\"\n", - " return dialog_state[-1]\n", - "\n", - "\n", - "builder.add_conditional_edges(\"fetch_user_info\", route_to_workflow)\n", - "\n", - "# Compile graph\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "part_4_graph = builder.compile(\n", - " checkpointer=memory,\n", - " # Let the user approve or deny the use of sensitive tools\n", - " interrupt_before=[\n", - " \"update_flight_sensitive_tools\",\n", - " \"book_car_rental_sensitive_tools\",\n", - " \"book_hotel_sensitive_tools\",\n", - " \"book_excursion_sensitive_tools\",\n", - " ],\n", - ")" - ] + "source": ["# Primary assistant\nbuilder.add_node(\"primary_assistant\", Assistant(assistant_runnable))\nbuilder.add_node(\n \"primary_assistant_tools\", create_tool_node_with_fallback(primary_assistant_tools)\n)\n\n\ndef route_primary_assistant(\n state: State,\n) -> Literal[\n \"primary_assistant_tools\",\n \"enter_update_flight\",\n \"enter_book_hotel\",\n \"enter_book_excursion\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n if tool_calls:\n if tool_calls[0][\"name\"] == ToFlightBookingAssistant.__name__:\n return \"enter_update_flight\"\n elif tool_calls[0][\"name\"] == ToBookCarRental.__name__:\n return \"enter_book_car_rental\"\n elif tool_calls[0][\"name\"] == ToHotelBookingAssistant.__name__:\n return \"enter_book_hotel\"\n elif tool_calls[0][\"name\"] == ToBookExcursion.__name__:\n return \"enter_book_excursion\"\n return \"primary_assistant_tools\"\n raise ValueError(\"Invalid route\")\n\n\n# The assistant can route to one of the delegated assistants,\n# directly use a tool, or directly respond to the user\nbuilder.add_conditional_edges(\n \"primary_assistant\",\n route_primary_assistant,\n {\n \"enter_update_flight\": \"enter_update_flight\",\n \"enter_book_car_rental\": \"enter_book_car_rental\",\n \"enter_book_hotel\": \"enter_book_hotel\",\n \"enter_book_excursion\": \"enter_book_excursion\",\n \"primary_assistant_tools\": \"primary_assistant_tools\",\n END: END,\n },\n)\nbuilder.add_edge(\"primary_assistant_tools\", \"primary_assistant\")\n\n\n# Each delegated workflow can directly respond to the user\n# When the user responds, we want to return to the currently active workflow\ndef route_to_workflow(\n state: State,\n) -> Literal[\n \"primary_assistant\",\n \"update_flight\",\n \"book_car_rental\",\n \"book_hotel\",\n \"book_excursion\",\n]:\n \"\"\"If we are in a delegated state, route directly to the appropriate assistant.\"\"\"\n dialog_state = state.get(\"dialog_state\")\n if not dialog_state:\n return \"primary_assistant\"\n return dialog_state[-1]\n\n\nbuilder.add_conditional_edges(\"fetch_user_info\", route_to_workflow)\n\n# Compile graph\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_4_graph = builder.compile(\n checkpointer=memory,\n # Let the user approve or deny the use of sensitive tools\n interrupt_before=[\n \"update_flight_sensitive_tools\",\n \"book_car_rental_sensitive_tools\",\n \"book_hotel_sensitive_tools\",\n \"book_excursion_sensitive_tools\",\n ],\n)"] }, { "cell_type": "code", @@ -3837,15 +1852,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(part_4_graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_4_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -4319,63 +2326,7 @@ ] } ], - "source": [ - "import shutil\n", - "import uuid\n", - "\n", - "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", - "thread_id = str(uuid.uuid4())\n", - "\n", - "config = {\n", - " \"configurable\": {\n", - " # The passenger_id is used in our flight tools to\n", - " # fetch the user's flight information\n", - " \"passenger_id\": \"3442 587242\",\n", - " # Checkpoints are accessed by thread_id\n", - " \"thread_id\": thread_id,\n", - " }\n", - "}\n", - "\n", - "_printed = set()\n", - "# We can reuse the tutorial questions from part 1 to see how it does.\n", - "for question in tutorial_questions:\n", - " events = part_4_graph.stream(\n", - " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n", - " )\n", - " for event in events:\n", - " _print_event(event, _printed)\n", - " snapshot = part_4_graph.get_state(config)\n", - " while snapshot.next:\n", - " # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n", - " # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n", - " # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n", - " user_input = input(\n", - " \"Do you approve of the above actions? Type 'y' to continue;\"\n", - " \" otherwise, explain your requested changed.\\n\\n\"\n", - " )\n", - " if user_input.strip() == \"y\":\n", - " # Just continue\n", - " result = part_4_graph.invoke(\n", - " None,\n", - " config,\n", - " )\n", - " else:\n", - " # Satisfy the tool invocation by\n", - " # providing instructions on the requested changes / change of mind\n", - " result = part_4_graph.invoke(\n", - " {\n", - " \"messages\": [\n", - " ToolMessage(\n", - " tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n", - " content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n", - " )\n", - " ]\n", - " },\n", - " config,\n", - " )\n", - " snapshot = part_4_graph.get_state(config)" - ] + "source": ["import shutil\nimport uuid\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\n_printed = set()\n# We can reuse the tutorial questions from part 1 to see how it does.\nfor question in tutorial_questions:\n events = part_4_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)\n snapshot = part_4_graph.get_state(config)\n while snapshot.next:\n # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n user_input = input(\n \"Do you approve of the above actions? Type 'y' to continue;\"\n \" otherwise, explain your requested changed.\\n\\n\"\n )\n if user_input.strip() == \"y\":\n # Just continue\n result = part_4_graph.invoke(\n None,\n config,\n )\n else:\n # Satisfy the tool invocation by\n # providing instructions on the requested changes / change of mind\n result = part_4_graph.invoke(\n {\n \"messages\": [\n ToolMessage(\n tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n )\n ]\n },\n config,\n )\n snapshot = part_4_graph.get_state(config)"] }, { "cell_type": "markdown", diff --git a/examples/docs/quickstart.ipynb b/examples/docs/quickstart.ipynb index e130c27f4..e8d621a80 100644 --- a/examples/docs/quickstart.ipynb +++ b/examples/docs/quickstart.ipynb @@ -12,46 +12,21 @@ "execution_count": 13, "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain-openai" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-openai"] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "if not os.environ.get(\"OPENAI_API_KEY\"):\n", - " os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" - ] + "source": ["import getpass\nimport os\n\nif not os.environ.get(\"OPENAI_API_KEY\"):\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import BaseMessage, HumanMessage\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "from langgraph.graph import END, MessageGraph\n", - "\n", - "model = ChatOpenAI(temperature=0)\n", - "\n", - "graph = MessageGraph()\n", - "\n", - "graph.add_node(\"oracle\", model)\n", - "graph.add_edge(\"oracle\", END)\n", - "\n", - "graph.set_entry_point(\"oracle\")\n", - "\n", - "runnable = graph.compile()" - ] + "source": ["from langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, MessageGraph\n\nmodel = ChatOpenAI(temperature=0)\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model)\ngraph.add_edge(\"oracle\", END)\n\ngraph.add_edge(START, \"oracle\")\n\nrunnable = graph.compile()"] }, { "cell_type": "code", @@ -69,15 +44,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "code", @@ -96,54 +63,14 @@ "output_type": "execute_result" } ], - "source": [ - "runnable.invoke(HumanMessage(\"What is 1 + 1?\"))" - ] + "source": ["runnable.invoke(HumanMessage(\"What is 1 + 1?\"))"] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "from langchain_core.tools import tool\n", - "\n", - "from langgraph.graph import END, START\n", - "from langgraph.prebuilt import ToolNode\n", - "\n", - "\n", - "@tool\n", - "def multiply(first_number: int, second_number: int):\n", - " \"\"\"Multiplies two numbers together.\"\"\"\n", - " return first_number * second_number\n", - "\n", - "\n", - "model = ChatOpenAI(temperature=0)\n", - "model_with_tools = model.bind_tools(tools=[multiply])\n", - "\n", - "graph = MessageGraph()\n", - "\n", - "graph.add_node(\"oracle\", model_with_tools)\n", - "\n", - "tool_node = ToolNode([multiply])\n", - "graph.add_node(\"multiply\", tool_node)\n", - "graph.add_edge(START, \"oracle\")\n", - "graph.add_edge(\"multiply\", END)\n", - "\n", - "\n", - "def router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n", - " tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n", - " if len(tool_calls):\n", - " return \"multiply\"\n", - " else:\n", - " return END\n", - "\n", - "\n", - "graph.add_conditional_edges(\"oracle\", router)\n", - "runnable = graph.compile()" - ] + "source": ["from typing import Literal\n\nfrom langchain_core.tools import tool\n\nfrom langgraph.graph import END, START\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef multiply(first_number: int, second_number: int):\n \"\"\"Multiplies two numbers together.\"\"\"\n return first_number * second_number\n\n\nmodel = ChatOpenAI(temperature=0)\nmodel_with_tools = model.bind_tools(tools=[multiply])\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model_with_tools)\n\ntool_node = ToolNode([multiply])\ngraph.add_node(\"multiply\", tool_node)\ngraph.add_edge(START, \"oracle\")\ngraph.add_edge(\"multiply\", END)\n\n\ndef router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n if len(tool_calls):\n return \"multiply\"\n else:\n return END\n\n\ngraph.add_conditional_edges(\"oracle\", router)\nrunnable = graph.compile()"] }, { "cell_type": "code", @@ -161,13 +88,7 @@ "output_type": "display_data" } ], - "source": [ - "try:\n", - " display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["try:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "code", @@ -187,9 +108,7 @@ "output_type": "execute_result" } ], - "source": [ - "runnable.invoke(HumanMessage(\"What is 123 * 456?\"))" - ] + "source": ["runnable.invoke(HumanMessage(\"What is 123 * 456?\"))"] }, { "cell_type": "code", @@ -208,16 +127,14 @@ "output_type": "execute_result" } ], - "source": [ - "runnable.invoke(HumanMessage(\"What is your name?\"))" - ] + "source": ["runnable.invoke(HumanMessage(\"What is your name?\"))"] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/dynamically-returning-directly.ipynb b/examples/dynamically-returning-directly.ipynb index 0a55e4046..118f9f75a 100644 --- a/examples/dynamically-returning-directly.ipynb +++ b/examples/dynamically-returning-directly.ipynb @@ -28,10 +28,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_community langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_community langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -47,19 +44,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")\n", - "_set_env(\"TAVILY_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")"] }, { "cell_type": "markdown", @@ -75,10 +60,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -104,19 +86,7 @@ "id": "4a1b9990-3b11-4a51-bd51-76117afd38b9", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class SearchTool(BaseModel):\n", - " \"\"\"Look up things online, optionally returning directly\"\"\"\n", - "\n", - " query: str = Field(description=\"query to look up online\")\n", - " return_direct: bool = Field(\n", - " description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\",\n", - " default=False,\n", - " )" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass SearchTool(BaseModel):\n \"\"\"Look up things online, optionally returning directly\"\"\"\n\n query: str = Field(description=\"query to look up online\")\n return_direct: bool = Field(\n description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\",\n default=False,\n )"] }, { "cell_type": "code", @@ -124,12 +94,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\n", - "tools = [search_tool]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\nsearch_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\ntools = [search_tool]"] }, { "cell_type": "markdown", @@ -147,11 +112,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -175,11 +136,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -197,9 +154,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -226,14 +181,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[list, operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict\n\n\nclass AgentState(TypedDict):\n messages: Annotated[list, operator.add]"] }, { "cell_type": "markdown", @@ -268,11 +216,7 @@ "id": "03308b6b-de72-4cdc-b6c6-47e654df340e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation"] }, { "cell_type": "markdown", @@ -290,22 +234,7 @@ "id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we check if it's suppose to return direct\n", - " else:\n", - " arguments = last_message.tool_calls[0][\"args\"]\n", - " if arguments.get(\"return_direct\", False):\n", - " return \"final\"\n", - " else:\n", - " return \"continue\"" - ] + "source": ["# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we check if it's suppose to return direct\n else:\n arguments = last_message.tool_calls[0][\"args\"]\n if arguments.get(\"return_direct\", False):\n return \"final\"\n else:\n return \"continue\""] }, { "cell_type": "code", @@ -313,14 +242,7 @@ "id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "markdown", @@ -338,33 +260,7 @@ "id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991", "metadata": {}, "outputs": [], - "source": [ - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " tool_call = last_message.tool_calls[0]\n", - " tool_name = tool_call[\"name\"]\n", - " arguments = tool_call[\"args\"]\n", - " if tool_name == \"tavily_search_results_json\":\n", - " if \"return_direct\" in arguments:\n", - " del arguments[\"return_direct\"]\n", - " action = ToolInvocation(\n", - " tool=tool_name,\n", - " tool_input=arguments,\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a ToolMessage\n", - " tool_message = ToolMessage(\n", - " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", - " )\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n tool_name = tool_call[\"name\"]\n arguments = tool_call[\"args\"]\n if tool_name == \"tavily_search_results_json\":\n if \"return_direct\" in arguments:\n del arguments[\"return_direct\"]\n action = ToolInvocation(\n tool=tool_name,\n tool_input=arguments,\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -386,56 +282,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "\n", - "# Note the \"action\" and \"final\" nodes are identical!\n", - "workflow.add_node(\"action\", call_tool)\n", - "workflow.add_node(\"final\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Final call\n", - " \"final\": \"final\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "workflow.add_edge(\"final\", END)\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\n\n# Note the \"action\" and \"final\" nodes are identical!\nworkflow.add_node(\"action\", call_tool)\nworkflow.add_node(\"final\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Final call\n \"final\": \"final\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\nworkflow.add_edge(\"final\", END)\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -454,11 +301,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -502,18 +345,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -563,22 +395,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"what is the weather in sf? return this result directly by setting return_direct = True\"\n", - " )\n", - " ]\n", - "}\n", - "for output in app.stream(inputs, stream_mode=\"values\"):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for message in output[\"messages\"]:\n", - " message.pretty_print()\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\n \"messages\": [\n HumanMessage(\n content=\"what is the weather in sf? return this result directly by setting return_direct = True\"\n )\n ]\n}\nfor output in app.stream(inputs, stream_mode=\"values\"):\n # stream() yields dictionaries with output keyed by node name\n for message in output[\"messages\"]:\n message.pretty_print()\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -586,7 +403,7 @@ "id": "49ccc134-4abe-4982-8ecd-d70fc56a4d2d", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/extraction/retries.ipynb b/examples/extraction/retries.ipynb index ca9bcf59f..2d240c9ad 100644 --- a/examples/extraction/retries.ipynb +++ b/examples/extraction/retries.ipynb @@ -32,11 +32,7 @@ "id": "0ada5e8f-3f2f-459e-83aa-6cd8861770dd", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langchain-anthropic langgraph\n", - "# Or do langchain-{groq|openai|etc.} for another package with tool calling" - ] + "source": ["%%capture --no-stderr\n%pip install -U langchain-anthropic langgraph\n# Or do langchain-{groq|openai|etc.} for another package with tool calling"] }, { "cell_type": "markdown", @@ -52,22 +48,7 @@ "id": "c0acb818-b6fd-48ab-97e6-fc2de2d03e87", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")\n", - "# Recommended to visualize the retry steps\n", - "_set_env(\"LANGCHAIN_API_KEY\")\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"Extraction Notebook\"" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")\n# Recommended to visualize the retry steps\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Extraction Notebook\""] }, { "cell_type": "markdown", @@ -83,289 +64,7 @@ "id": "baf669a0-04ee-492d-80d8-8fcb658ed128", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "import uuid\n", - "from typing import (\n", - " Annotated,\n", - " Any,\n", - " Callable,\n", - " Dict,\n", - " List,\n", - " Literal,\n", - " Optional,\n", - " Sequence,\n", - " Type,\n", - " Union,\n", - ")\n", - "\n", - "from langchain_core.language_models import BaseChatModel\n", - "from langchain_core.messages import (\n", - " AIMessage,\n", - " AnyMessage,\n", - " BaseMessage,\n", - " HumanMessage,\n", - " ToolCall,\n", - ")\n", - "from langchain_core.prompt_values import PromptValue\n", - "from langchain_core.runnables import (\n", - " Runnable,\n", - " RunnableLambda,\n", - ")\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ValidationNode\n", - "\n", - "\n", - "def _default_aggregator(messages: Sequence[AnyMessage]) -> AIMessage:\n", - " for m in messages[::-1]:\n", - " if m.type == \"ai\":\n", - " return m\n", - " raise ValueError(\"No AI message found in the sequence.\")\n", - "\n", - "\n", - "class RetryStrategy(TypedDict, total=False):\n", - " \"\"\"The retry strategy for a tool call.\"\"\"\n", - "\n", - " max_attempts: int\n", - " \"\"\"The maximum number of attempts to make.\"\"\"\n", - " fallback: Optional[\n", - " Union[\n", - " Runnable[Sequence[AnyMessage], AIMessage],\n", - " Runnable[Sequence[AnyMessage], BaseMessage],\n", - " Callable[[Sequence[AnyMessage]], AIMessage],\n", - " ]\n", - " ]\n", - " \"\"\"The function to use once validation fails.\"\"\"\n", - " aggregate_messages: Optional[Callable[[Sequence[AnyMessage]], AIMessage]]\n", - "\n", - "\n", - "def _bind_validator_with_retries(\n", - " llm: Union[\n", - " Runnable[Sequence[AnyMessage], AIMessage],\n", - " Runnable[Sequence[BaseMessage], BaseMessage],\n", - " ],\n", - " *,\n", - " validator: ValidationNode,\n", - " retry_strategy: RetryStrategy,\n", - " tool_choice: Optional[str] = None,\n", - ") -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n", - " \"\"\"Binds a tool validators + retry logic to create a runnable validation graph.\n", - "\n", - " LLMs that support tool calling can generate structured JSON. However, they may not always\n", - " perfectly follow your requested schema, especially if the schema is nested or has complex\n", - " validation rules. This method allows you to bind a validation function to the LLM's output,\n", - " so that any time the LLM generates a message, the validation function is run on it. If\n", - " the validation fails, the method will retry the LLM with a fallback strategy, the simplest\n", - " being just to add a message to the output with the validation errors and a request to fix them.\n", - "\n", - " The resulting runnable expects a list of messages as input and returns a single AI message.\n", - " By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n", - " your existing chat bot. You can specify a tool_choice to force the validator to be run on\n", - " the outputs.\n", - "\n", - " Args:\n", - " llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n", - " validator (ValidationNode): The validation logic.\n", - " retry_strategy (RetryStrategy): The retry strategy to use.\n", - " Possible keys:\n", - " - max_attempts: The maximum number of attempts to make.\n", - " - fallback: The LLM or function to use in case of validation failure.\n", - " - aggregate_messages: A function to aggregate the messages over multiple turns.\n", - " Defaults to fetching the last AI message.\n", - " tool_choice: If provided, always run the validator on the tool output.\n", - "\n", - " Returns:\n", - " Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n", - " \"\"\"\n", - "\n", - " def add_or_overwrite_messages(left: list, right: Union[list, dict]) -> list:\n", - " \"\"\"Append messages. If the update is a 'finalized' output, replace the whole list.\"\"\"\n", - " if isinstance(right, dict) and \"finalize\" in right:\n", - " finalized = right[\"finalize\"]\n", - " if not isinstance(finalized, list):\n", - " finalized = [finalized]\n", - " for m in finalized:\n", - " if m.id is None:\n", - " m.id = str(uuid.uuid4())\n", - " return finalized\n", - " res = add_messages(left, right)\n", - " if not isinstance(res, list):\n", - " return [res]\n", - " return res\n", - "\n", - " class State(TypedDict):\n", - " messages: Annotated[list, add_or_overwrite_messages]\n", - " attempt_number: Annotated[int, operator.add]\n", - " initial_num_messages: int\n", - " input_format: Literal[\"list\", \"dict\"]\n", - "\n", - " builder = StateGraph(State)\n", - "\n", - " def dedict(x: State) -> list:\n", - " \"\"\"Get the messages from the state.\"\"\"\n", - " return x[\"messages\"]\n", - "\n", - " model = dedict | llm | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n", - " fbrunnable = retry_strategy.get(\"fallback\")\n", - " if fbrunnable is None:\n", - " fb_runnable = llm\n", - " elif isinstance(fbrunnable, Runnable):\n", - " fb_runnable = fbrunnable # type: ignore\n", - " else:\n", - " fb_runnable = RunnableLambda(fbrunnable)\n", - " fallback = (\n", - " dedict | fb_runnable | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n", - " )\n", - "\n", - " def count_messages(state: State) -> dict:\n", - " return {\"initial_num_messages\": len(state.get(\"messages\", []))}\n", - "\n", - " builder.add_node(\"count_messages\", count_messages)\n", - " builder.add_node(\"llm\", model)\n", - " builder.add_node(\"fallback\", fallback)\n", - "\n", - " # To support patch-based retries, we need to be able to\n", - " # aggregate the messages over multiple turns.\n", - " # The next sequence selects only the relevant messages\n", - " # and then applies the validator\n", - " select_messages = retry_strategy.get(\"aggregate_messages\") or _default_aggregator\n", - "\n", - " def select_generated_messages(state: State) -> list:\n", - " \"\"\"Select only the messages generated within this loop.\"\"\"\n", - " selected = state[\"messages\"][state[\"initial_num_messages\"] :]\n", - " return [select_messages(selected)]\n", - "\n", - " def endict_validator_output(x: Sequence[AnyMessage]) -> dict:\n", - " if tool_choice and not x:\n", - " return {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=f\"ValidationError: please respond with a valid tool call [tool_choice={tool_choice}].\",\n", - " additional_kwargs={\"is_error\": True},\n", - " )\n", - " ]\n", - " }\n", - " return {\"messages\": x}\n", - "\n", - " validator_runnable = select_generated_messages | validator | endict_validator_output\n", - " builder.add_node(\"validator\", validator_runnable)\n", - "\n", - " class Finalizer:\n", - " \"\"\"Pick the final message to return from the retry loop.\"\"\"\n", - "\n", - " def __init__(self, aggregator: Optional[Callable[[list], AIMessage]] = None):\n", - " self._aggregator = aggregator or _default_aggregator\n", - "\n", - " def __call__(self, state: State) -> dict:\n", - " \"\"\"Return just the AI message.\"\"\"\n", - " initial_num_messages = state[\"initial_num_messages\"]\n", - " generated_messages = state[\"messages\"][initial_num_messages:]\n", - " return {\n", - " \"messages\": {\n", - " \"finalize\": self._aggregator(generated_messages),\n", - " }\n", - " }\n", - "\n", - " # We only want to emit the final message\n", - " builder.add_node(\"finalizer\", Finalizer(retry_strategy.get(\"aggregate_messages\")))\n", - "\n", - " # Define the connectivity\n", - " builder.set_entry_point(\"count_messages\")\n", - " builder.add_edge(\"count_messages\", \"llm\")\n", - "\n", - " def route_validator(state: State) -> Literal[\"validator\", \"__end__\"]:\n", - " if state[\"messages\"][-1].tool_calls or tool_choice is not None:\n", - " return \"validator\"\n", - " return \"__end__\"\n", - "\n", - " builder.add_conditional_edges(\"llm\", route_validator)\n", - " builder.add_edge(\"fallback\", \"validator\")\n", - " max_attempts = retry_strategy.get(\"max_attempts\", 3)\n", - "\n", - " def route_validation(state: State) -> Literal[\"finalizer\", \"fallback\"]:\n", - " if state[\"attempt_number\"] > max_attempts:\n", - " raise ValueError(\n", - " f\"Could not extract a valid value in {max_attempts} attempts.\"\n", - " )\n", - " for m in state[\"messages\"][::-1]:\n", - " if m.type == \"ai\":\n", - " break\n", - " if m.additional_kwargs.get(\"is_error\"):\n", - " return \"fallback\"\n", - " return \"finalizer\"\n", - "\n", - " builder.add_conditional_edges(\"validator\", route_validation)\n", - "\n", - " builder.set_finish_point(\"finalizer\")\n", - "\n", - " # These functions let the step be used in a MessageGraph\n", - " # or a StateGraph with 'messages' as the key.\n", - " def encode(x: Union[Sequence[AnyMessage], PromptValue]) -> dict:\n", - " \"\"\"Ensure the input is the correct format.\"\"\"\n", - " if isinstance(x, PromptValue):\n", - " return {\"messages\": x.to_messages(), \"input_format\": \"list\"}\n", - " if isinstance(x, list):\n", - " return {\"messages\": x, \"input_format\": \"list\"}\n", - " raise ValueError(f\"Unexpected input type: {type(x)}\")\n", - "\n", - " def decode(x: State) -> AIMessage:\n", - " \"\"\"Ensure the output is in the expected format.\"\"\"\n", - " return x[\"messages\"][-1]\n", - "\n", - " return (\n", - " encode | builder.compile().with_config(run_name=\"ValidationGraph\") | decode\n", - " ).with_config(run_name=\"ValidateWithRetries\")\n", - "\n", - "\n", - "def bind_validator_with_retries(\n", - " llm: BaseChatModel,\n", - " *,\n", - " tools: list,\n", - " tool_choice: Optional[str] = None,\n", - " max_attempts: int = 3,\n", - ") -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n", - " \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n", - "\n", - " LLMs that support tool calling are good at generating structured JSON. However, they may\n", - " not always perfectly follow your requested schema, especially if the schema is nested or\n", - " has complex validation rules. This method allows you to bind a validation function to\n", - " the LLM's output, so that any time the LLM generates a message, the validation function\n", - " is run on it. If the validation fails, the method will retry the LLM with a fallback\n", - " strategy, the simples being just to add a message to the output with the validation\n", - " errors and a request to fix them.\n", - "\n", - " The resulting runnable expects a list of messages as input and returns a single AI message.\n", - " By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n", - " your existing chat bot. You can specify a tool_choice to force the validator to be run on\n", - " the outputs.\n", - "\n", - " Args:\n", - " llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n", - " validator (ValidationNode): The validation logic.\n", - " retry_strategy (RetryStrategy): The retry strategy to use.\n", - " Possible keys:\n", - " - max_attempts: The maximum number of attempts to make.\n", - " - fallback: The LLM or function to use in case of validation failure.\n", - " - aggregate_messages: A function to aggregate the messages over multiple turns.\n", - " Defaults to fetching the last AI message.\n", - " tool_choice: If provided, always run the validator on the tool output.\n", - "\n", - " Returns:\n", - " Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n", - " \"\"\"\n", - " bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n", - " retry_strategy = RetryStrategy(max_attempts=max_attempts)\n", - " validator = ValidationNode(tools)\n", - " return _bind_validator_with_retries(\n", - " bound_llm,\n", - " validator=validator,\n", - " tool_choice=tool_choice,\n", - " retry_strategy=retry_strategy,\n", - " ).with_config(metadata={\"retry_strategy\": \"default\"})" - ] + "source": ["import operator\nimport uuid\nfrom typing import (\n Annotated,\n Any,\n Callable,\n Dict,\n List,\n Literal,\n Optional,\n Sequence,\n Type,\n Union,\n)\n\nfrom langchain_core.language_models import BaseChatModel\nfrom langchain_core.messages import (\n AIMessage,\n AnyMessage,\n BaseMessage,\n HumanMessage,\n ToolCall,\n)\nfrom langchain_core.prompt_values import PromptValue\nfrom langchain_core.runnables import (\n Runnable,\n RunnableLambda,\n)\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ValidationNode\n\n\ndef _default_aggregator(messages: Sequence[AnyMessage]) -> AIMessage:\n for m in messages[::-1]:\n if m.type == \"ai\":\n return m\n raise ValueError(\"No AI message found in the sequence.\")\n\n\nclass RetryStrategy(TypedDict, total=False):\n \"\"\"The retry strategy for a tool call.\"\"\"\n\n max_attempts: int\n \"\"\"The maximum number of attempts to make.\"\"\"\n fallback: Optional[\n Union[\n Runnable[Sequence[AnyMessage], AIMessage],\n Runnable[Sequence[AnyMessage], BaseMessage],\n Callable[[Sequence[AnyMessage]], AIMessage],\n ]\n ]\n \"\"\"The function to use once validation fails.\"\"\"\n aggregate_messages: Optional[Callable[[Sequence[AnyMessage]], AIMessage]]\n\n\ndef _bind_validator_with_retries(\n llm: Union[\n Runnable[Sequence[AnyMessage], AIMessage],\n Runnable[Sequence[BaseMessage], BaseMessage],\n ],\n *,\n validator: ValidationNode,\n retry_strategy: RetryStrategy,\n tool_choice: Optional[str] = None,\n) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n \"\"\"Binds a tool validators + retry logic to create a runnable validation graph.\n\n LLMs that support tool calling can generate structured JSON. However, they may not always\n perfectly follow your requested schema, especially if the schema is nested or has complex\n validation rules. This method allows you to bind a validation function to the LLM's output,\n so that any time the LLM generates a message, the validation function is run on it. If\n the validation fails, the method will retry the LLM with a fallback strategy, the simplest\n being just to add a message to the output with the validation errors and a request to fix them.\n\n The resulting runnable expects a list of messages as input and returns a single AI message.\n By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n your existing chat bot. You can specify a tool_choice to force the validator to be run on\n the outputs.\n\n Args:\n llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n validator (ValidationNode): The validation logic.\n retry_strategy (RetryStrategy): The retry strategy to use.\n Possible keys:\n - max_attempts: The maximum number of attempts to make.\n - fallback: The LLM or function to use in case of validation failure.\n - aggregate_messages: A function to aggregate the messages over multiple turns.\n Defaults to fetching the last AI message.\n tool_choice: If provided, always run the validator on the tool output.\n\n Returns:\n Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n \"\"\"\n\n def add_or_overwrite_messages(left: list, right: Union[list, dict]) -> list:\n \"\"\"Append messages. If the update is a 'finalized' output, replace the whole list.\"\"\"\n if isinstance(right, dict) and \"finalize\" in right:\n finalized = right[\"finalize\"]\n if not isinstance(finalized, list):\n finalized = [finalized]\n for m in finalized:\n if m.id is None:\n m.id = str(uuid.uuid4())\n return finalized\n res = add_messages(left, right)\n if not isinstance(res, list):\n return [res]\n return res\n\n class State(TypedDict):\n messages: Annotated[list, add_or_overwrite_messages]\n attempt_number: Annotated[int, operator.add]\n initial_num_messages: int\n input_format: Literal[\"list\", \"dict\"]\n\n builder = StateGraph(State)\n\n def dedict(x: State) -> list:\n \"\"\"Get the messages from the state.\"\"\"\n return x[\"messages\"]\n\n model = dedict | llm | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n fbrunnable = retry_strategy.get(\"fallback\")\n if fbrunnable is None:\n fb_runnable = llm\n elif isinstance(fbrunnable, Runnable):\n fb_runnable = fbrunnable # type: ignore\n else:\n fb_runnable = RunnableLambda(fbrunnable)\n fallback = (\n dedict | fb_runnable | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n )\n\n def count_messages(state: State) -> dict:\n return {\"initial_num_messages\": len(state.get(\"messages\", []))}\n\n builder.add_node(\"count_messages\", count_messages)\n builder.add_node(\"llm\", model)\n builder.add_node(\"fallback\", fallback)\n\n # To support patch-based retries, we need to be able to\n # aggregate the messages over multiple turns.\n # The next sequence selects only the relevant messages\n # and then applies the validator\n select_messages = retry_strategy.get(\"aggregate_messages\") or _default_aggregator\n\n def select_generated_messages(state: State) -> list:\n \"\"\"Select only the messages generated within this loop.\"\"\"\n selected = state[\"messages\"][state[\"initial_num_messages\"] :]\n return [select_messages(selected)]\n\n def endict_validator_output(x: Sequence[AnyMessage]) -> dict:\n if tool_choice and not x:\n return {\n \"messages\": [\n HumanMessage(\n content=f\"ValidationError: please respond with a valid tool call [tool_choice={tool_choice}].\",\n additional_kwargs={\"is_error\": True},\n )\n ]\n }\n return {\"messages\": x}\n\n validator_runnable = select_generated_messages | validator | endict_validator_output\n builder.add_node(\"validator\", validator_runnable)\n\n class Finalizer:\n \"\"\"Pick the final message to return from the retry loop.\"\"\"\n\n def __init__(self, aggregator: Optional[Callable[[list], AIMessage]] = None):\n self._aggregator = aggregator or _default_aggregator\n\n def __call__(self, state: State) -> dict:\n \"\"\"Return just the AI message.\"\"\"\n initial_num_messages = state[\"initial_num_messages\"]\n generated_messages = state[\"messages\"][initial_num_messages:]\n return {\n \"messages\": {\n \"finalize\": self._aggregator(generated_messages),\n }\n }\n\n # We only want to emit the final message\n builder.add_node(\"finalizer\", Finalizer(retry_strategy.get(\"aggregate_messages\")))\n\n # Define the connectivity\n builder.add_edge(START, \"count_messages\")\n builder.add_edge(\"count_messages\", \"llm\")\n\n def route_validator(state: State) -> Literal[\"validator\", \"__end__\"]:\n if state[\"messages\"][-1].tool_calls or tool_choice is not None:\n return \"validator\"\n return \"__end__\"\n\n builder.add_conditional_edges(\"llm\", route_validator)\n builder.add_edge(\"fallback\", \"validator\")\n max_attempts = retry_strategy.get(\"max_attempts\", 3)\n\n def route_validation(state: State) -> Literal[\"finalizer\", \"fallback\"]:\n if state[\"attempt_number\"] > max_attempts:\n raise ValueError(\n f\"Could not extract a valid value in {max_attempts} attempts.\"\n )\n for m in state[\"messages\"][::-1]:\n if m.type == \"ai\":\n break\n if m.additional_kwargs.get(\"is_error\"):\n return \"fallback\"\n return \"finalizer\"\n\n builder.add_conditional_edges(\"validator\", route_validation)\n\n builder.set_finish_point(\"finalizer\")\n\n # These functions let the step be used in a MessageGraph\n # or a StateGraph with 'messages' as the key.\n def encode(x: Union[Sequence[AnyMessage], PromptValue]) -> dict:\n \"\"\"Ensure the input is the correct format.\"\"\"\n if isinstance(x, PromptValue):\n return {\"messages\": x.to_messages(), \"input_format\": \"list\"}\n if isinstance(x, list):\n return {\"messages\": x, \"input_format\": \"list\"}\n raise ValueError(f\"Unexpected input type: {type(x)}\")\n\n def decode(x: State) -> AIMessage:\n \"\"\"Ensure the output is in the expected format.\"\"\"\n return x[\"messages\"][-1]\n\n return (\n encode | builder.compile().with_config(run_name=\"ValidationGraph\") | decode\n ).with_config(run_name=\"ValidateWithRetries\")\n\n\ndef bind_validator_with_retries(\n llm: BaseChatModel,\n *,\n tools: list,\n tool_choice: Optional[str] = None,\n max_attempts: int = 3,\n) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n\n LLMs that support tool calling are good at generating structured JSON. However, they may\n not always perfectly follow your requested schema, especially if the schema is nested or\n has complex validation rules. This method allows you to bind a validation function to\n the LLM's output, so that any time the LLM generates a message, the validation function\n is run on it. If the validation fails, the method will retry the LLM with a fallback\n strategy, the simples being just to add a message to the output with the validation\n errors and a request to fix them.\n\n The resulting runnable expects a list of messages as input and returns a single AI message.\n By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n your existing chat bot. You can specify a tool_choice to force the validator to be run on\n the outputs.\n\n Args:\n llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n validator (ValidationNode): The validation logic.\n retry_strategy (RetryStrategy): The retry strategy to use.\n Possible keys:\n - max_attempts: The maximum number of attempts to make.\n - fallback: The LLM or function to use in case of validation failure.\n - aggregate_messages: A function to aggregate the messages over multiple turns.\n Defaults to fetching the last AI message.\n tool_choice: If provided, always run the validator on the tool output.\n\n Returns:\n Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n \"\"\"\n bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n retry_strategy = RetryStrategy(max_attempts=max_attempts)\n validator = ValidationNode(tools)\n return _bind_validator_with_retries(\n bound_llm,\n validator=validator,\n tool_choice=tool_choice,\n retry_strategy=retry_strategy,\n ).with_config(metadata={\"retry_strategy\": \"default\"})"] }, { "cell_type": "markdown", @@ -383,27 +82,7 @@ "id": "5df33c17-ee1a-409e-b5ec-f24e116da7d1", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field, validator\n", - "\n", - "\n", - "class Respond(BaseModel):\n", - " \"\"\"Use to generate the response. Always use when responding to the user\"\"\"\n", - "\n", - " reason: str = Field(description=\"Step-by-step justification for the answer.\")\n", - " answer: str\n", - "\n", - " @validator(\"answer\")\n", - " def reason_contains_apology(cls, answer: str):\n", - " if \"llama\" not in answer.lower():\n", - " raise ValueError(\n", - " \"You MUST start with a gimicky, rhyming advertisement for using a Llama V3 (an LLM) in your **answer** field.\"\n", - " \" Must be an instant hit. Must be weaved into the answer.\"\n", - " )\n", - "\n", - "\n", - "tools = [Respond]" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field, validator\n\n\nclass Respond(BaseModel):\n \"\"\"Use to generate the response. Always use when responding to the user\"\"\"\n\n reason: str = Field(description=\"Step-by-step justification for the answer.\")\n answer: str\n\n @validator(\"answer\")\n def reason_contains_apology(cls, answer: str):\n if \"llama\" not in answer.lower():\n raise ValueError(\n \"You MUST start with a gimicky, rhyming advertisement for using a Llama V3 (an LLM) in your **answer** field.\"\n \" Must be an instant hit. Must be weaved into the answer.\"\n )\n\n\ntools = [Respond]"] }, { "cell_type": "markdown", @@ -419,23 +98,7 @@ "id": "38231a5b-d018-41ee-a92c-2f2248edf417", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "\n", - "# Or you can use ChatGroq, ChatOpenAI, ChatGoogleGemini, ChatCohere, etc.\n", - "# See https://python.langchain.com/v0.2/docs/integrations/chat/ for more info on tool calling\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "bound_llm = bind_validator_with_retries(llm, tools=tools)\n", - "prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", \"Respond directly by calling the Respond function.\"),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ")\n", - "\n", - "chain = prompt | bound_llm" - ] + "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_core.prompts import ChatPromptTemplate\n\n# Or you can use ChatGroq, ChatOpenAI, ChatGoogleGemini, ChatCohere, etc.\n# See https://python.langchain.com/v0.2/docs/integrations/chat/ for more info on tool calling\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nbound_llm = bind_validator_with_retries(llm, tools=tools)\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", \"Respond directly by calling the Respond function.\"),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\nchain = prompt | bound_llm"] }, { "cell_type": "code", @@ -463,10 +126,7 @@ ] } ], - "source": [ - "results = chain.invoke({\"messages\": [(\"user\", \"Does P = NP?\")]})\n", - "results.pretty_print()" - ] + "source": ["results = chain.invoke({\"messages\": [(\"user\", \"Does P = NP?\")]})\nresults.pretty_print()"] }, { "cell_type": "markdown", @@ -486,97 +146,7 @@ "id": "f4f7438b-b6c1-48fd-b70f-185af7a2f64a", "metadata": {}, "outputs": [], - "source": [ - "from typing import List, Optional\n", - "\n", - "\n", - "class OutputFormat(BaseModel):\n", - " sources: str = Field(\n", - " ...,\n", - " description=\"The raw transcript / span you could cite to justify the choice.\",\n", - " )\n", - " content: str = Field(..., description=\"The chosen value.\")\n", - "\n", - "\n", - "class Moment(BaseModel):\n", - " quote: str = Field(..., description=\"The relevant quote from the transcript.\")\n", - " description: str = Field(..., description=\"A description of the moment.\")\n", - " expressed_preference: OutputFormat = Field(\n", - " ..., description=\"The preference expressed in the moment.\"\n", - " )\n", - "\n", - "\n", - "class BackgroundInfo(BaseModel):\n", - " factoid: OutputFormat = Field(\n", - " ..., description=\"Important factoid about the member.\"\n", - " )\n", - " professions: list\n", - " why: str = Field(..., description=\"Why this is important.\")\n", - "\n", - "\n", - "class KeyMoments(BaseModel):\n", - " topic: str = Field(..., description=\"The topic of the key moments.\")\n", - " happy_moments: List[Moment] = Field(\n", - " ..., description=\"A list of key moments related to the topic.\"\n", - " )\n", - " tense_moments: List[Moment] = Field(\n", - " ..., description=\"Moments where things were a bit tense.\"\n", - " )\n", - " sad_moments: List[Moment] = Field(\n", - " ..., description=\"Moments where things where everyone was downtrodden.\"\n", - " )\n", - " background_info: list[BackgroundInfo]\n", - " moments_summary: str = Field(..., description=\"A summary of the key moments.\")\n", - "\n", - "\n", - "class Member(BaseModel):\n", - " name: OutputFormat = Field(..., description=\"The name of the member.\")\n", - " role: Optional[str] = Field(None, description=\"The role of the member.\")\n", - " age: Optional[int] = Field(None, description=\"The age of the member.\")\n", - " background_details: List[BackgroundInfo] = Field(\n", - " ..., description=\"A list of background details about the member.\"\n", - " )\n", - "\n", - "\n", - "class InsightfulQuote(BaseModel):\n", - " quote: OutputFormat = Field(\n", - " ..., description=\"An insightful quote from the transcript.\"\n", - " )\n", - " speaker: str = Field(..., description=\"The name of the speaker who said the quote.\")\n", - " analysis: str = Field(\n", - " ..., description=\"An analysis of the quote and its significance.\"\n", - " )\n", - "\n", - "\n", - "class TranscriptMetadata(BaseModel):\n", - " title: str = Field(..., description=\"The title of the transcript.\")\n", - " location: OutputFormat = Field(\n", - " ..., description=\"The location where the interview took place.\"\n", - " )\n", - " duration: str = Field(..., description=\"The duration of the interview.\")\n", - "\n", - "\n", - "class TranscriptSummary(BaseModel):\n", - " metadata: TranscriptMetadata = Field(\n", - " ..., description=\"Metadata about the transcript.\"\n", - " )\n", - " participants: List[Member] = Field(\n", - " ..., description=\"A list of participants in the interview.\"\n", - " )\n", - " key_moments: List[KeyMoments] = Field(\n", - " ..., description=\"A list of key moments from the interview.\"\n", - " )\n", - " insightful_quotes: List[InsightfulQuote] = Field(\n", - " ..., description=\"A list of insightful quotes from the interview.\"\n", - " )\n", - " overall_summary: str = Field(\n", - " ..., description=\"An overall summary of the interview.\"\n", - " )\n", - " next_steps: List[str] = Field(\n", - " ..., description=\"A list of next steps or action items based on the interview.\"\n", - " )\n", - " other_stuff: List[OutputFormat]" - ] + "source": ["from typing import List, Optional\n\n\nclass OutputFormat(BaseModel):\n sources: str = Field(\n ...,\n description=\"The raw transcript / span you could cite to justify the choice.\",\n )\n content: str = Field(..., description=\"The chosen value.\")\n\n\nclass Moment(BaseModel):\n quote: str = Field(..., description=\"The relevant quote from the transcript.\")\n description: str = Field(..., description=\"A description of the moment.\")\n expressed_preference: OutputFormat = Field(\n ..., description=\"The preference expressed in the moment.\"\n )\n\n\nclass BackgroundInfo(BaseModel):\n factoid: OutputFormat = Field(\n ..., description=\"Important factoid about the member.\"\n )\n professions: list\n why: str = Field(..., description=\"Why this is important.\")\n\n\nclass KeyMoments(BaseModel):\n topic: str = Field(..., description=\"The topic of the key moments.\")\n happy_moments: List[Moment] = Field(\n ..., description=\"A list of key moments related to the topic.\"\n )\n tense_moments: List[Moment] = Field(\n ..., description=\"Moments where things were a bit tense.\"\n )\n sad_moments: List[Moment] = Field(\n ..., description=\"Moments where things where everyone was downtrodden.\"\n )\n background_info: list[BackgroundInfo]\n moments_summary: str = Field(..., description=\"A summary of the key moments.\")\n\n\nclass Member(BaseModel):\n name: OutputFormat = Field(..., description=\"The name of the member.\")\n role: Optional[str] = Field(None, description=\"The role of the member.\")\n age: Optional[int] = Field(None, description=\"The age of the member.\")\n background_details: List[BackgroundInfo] = Field(\n ..., description=\"A list of background details about the member.\"\n )\n\n\nclass InsightfulQuote(BaseModel):\n quote: OutputFormat = Field(\n ..., description=\"An insightful quote from the transcript.\"\n )\n speaker: str = Field(..., description=\"The name of the speaker who said the quote.\")\n analysis: str = Field(\n ..., description=\"An analysis of the quote and its significance.\"\n )\n\n\nclass TranscriptMetadata(BaseModel):\n title: str = Field(..., description=\"The title of the transcript.\")\n location: OutputFormat = Field(\n ..., description=\"The location where the interview took place.\"\n )\n duration: str = Field(..., description=\"The duration of the interview.\")\n\n\nclass TranscriptSummary(BaseModel):\n metadata: TranscriptMetadata = Field(\n ..., description=\"Metadata about the transcript.\"\n )\n participants: List[Member] = Field(\n ..., description=\"A list of participants in the interview.\"\n )\n key_moments: List[KeyMoments] = Field(\n ..., description=\"A list of key moments from the interview.\"\n )\n insightful_quotes: List[InsightfulQuote] = Field(\n ..., description=\"A list of insightful quotes from the interview.\"\n )\n overall_summary: str = Field(\n ..., description=\"An overall summary of the interview.\"\n )\n next_steps: List[str] = Field(\n ..., description=\"A list of next steps or action items based on the interview.\"\n )\n other_stuff: List[OutputFormat]"] }, { "cell_type": "markdown", @@ -592,82 +162,7 @@ "id": "e2d10886-7b1e-485f-91cd-1184a1c99303", "metadata": {}, "outputs": [], - "source": [ - "transcript = [\n", - " (\n", - " \"Pete\",\n", - " \"Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.\",\n", - " ),\n", - " (\n", - " \"Xu\",\n", - " \"No problem. As its my job, I've got some thoughts on this beef.\",\n", - " ),\n", - " (\n", - " \"Laura\",\n", - " \"Yeah, I've got some insider info so this should be interesting.\",\n", - " ),\n", - " (\"Pete\", \"Dope. So, when do you think this whole thing started?\"),\n", - " (\n", - " \"Pete\",\n", - " \"Definitely was Kendrick's 'Control' verse that kicked it off.\",\n", - " ),\n", - " (\n", - " \"Laura\",\n", - " \"Truth, but Drake never went after him directly. Just some subtle jabs here and there.\",\n", - " ),\n", - " (\n", - " \"Xu\",\n", - " \"That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.\",\n", - " ),\n", - " (\n", - " \"Pete\",\n", - " \"For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.\",\n", - " ),\n", - " (\n", - " \"Laura\",\n", - " \"I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.\",\n", - " ),\n", - " (\n", - " \"Pete\",\n", - " \"I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.\",\n", - " ),\n", - " (\n", - " \"Xu\",\n", - " \"It's wild how this beef is shaping fans.\",\n", - " ),\n", - " (\"Pete\", \"do you think these beefs can actually be good for hip-hop?\"),\n", - " (\n", - " \"Xu\",\n", - " \"Hell yeah, Pete. When it's done right, a beef can push the genre forward and make artists level up.\",\n", - " ),\n", - " (\"Laura\", \"eh\"),\n", - " (\"Pete\", \"So, where do you see this beef going?\"),\n", - " (\n", - " \"Laura\",\n", - " \"Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate.\",\n", - " ),\n", - " (\"Laura\", \"ehhhhhh not sure\"),\n", - " (\n", - " \"Pete\",\n", - " \"I feel that. I just want both of them to keep dropping heat, beef or no beef.\",\n", - " ),\n", - " (\n", - " \"Xu\",\n", - " \"I'm curious. May influence a lot of people. Make things more competitive. Bring on a whole new wave of lyricism.\",\n", - " ),\n", - " (\n", - " \"Pete\",\n", - " \"Word. Hey, thanks for chopping it up with me, Xu and Laura. This was dope.\",\n", - " ),\n", - " (\"Xu\", \"Where are you going so fast?\"),\n", - " (\n", - " \"Laura\",\n", - " \"For real, I had a good time. Nice to get different perspectives on the situation.\",\n", - " ),\n", - "]\n", - "\n", - "formatted = \"\\n\".join(f\"{x[0]}: {x[1]}\" for x in transcript)" - ] + "source": ["transcript = [\n (\n \"Pete\",\n \"Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.\",\n ),\n (\n \"Xu\",\n \"No problem. As its my job, I've got some thoughts on this beef.\",\n ),\n (\n \"Laura\",\n \"Yeah, I've got some insider info so this should be interesting.\",\n ),\n (\"Pete\", \"Dope. So, when do you think this whole thing started?\"),\n (\n \"Pete\",\n \"Definitely was Kendrick's 'Control' verse that kicked it off.\",\n ),\n (\n \"Laura\",\n \"Truth, but Drake never went after him directly. Just some subtle jabs here and there.\",\n ),\n (\n \"Xu\",\n \"That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.\",\n ),\n (\n \"Pete\",\n \"For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.\",\n ),\n (\n \"Laura\",\n \"I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.\",\n ),\n (\n \"Pete\",\n \"I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.\",\n ),\n (\n \"Xu\",\n \"It's wild how this beef is shaping fans.\",\n ),\n (\"Pete\", \"do you think these beefs can actually be good for hip-hop?\"),\n (\n \"Xu\",\n \"Hell yeah, Pete. When it's done right, a beef can push the genre forward and make artists level up.\",\n ),\n (\"Laura\", \"eh\"),\n (\"Pete\", \"So, where do you see this beef going?\"),\n (\n \"Laura\",\n \"Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate.\",\n ),\n (\"Laura\", \"ehhhhhh not sure\"),\n (\n \"Pete\",\n \"I feel that. I just want both of them to keep dropping heat, beef or no beef.\",\n ),\n (\n \"Xu\",\n \"I'm curious. May influence a lot of people. Make things more competitive. Bring on a whole new wave of lyricism.\",\n ),\n (\n \"Pete\",\n \"Word. Hey, thanks for chopping it up with me, Xu and Laura. This was dope.\",\n ),\n (\"Xu\", \"Where are you going so fast?\"),\n (\n \"Laura\",\n \"For real, I had a good time. Nice to get different perspectives on the situation.\",\n ),\n]\n\nformatted = \"\\n\".join(f\"{x[0]}: {x[1]}\" for x in transcript)"] }, { "cell_type": "markdown", @@ -710,34 +205,7 @@ ] } ], - "source": [ - "tools = [TranscriptSummary]\n", - "bound_llm = bind_validator_with_retries(\n", - " llm,\n", - " tools=tools,\n", - ")\n", - "prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", \"Respond directly using the TranscriptSummary function.\"),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ")\n", - "\n", - "chain = prompt | bound_llm\n", - "\n", - "results = chain.invoke(\n", - " {\n", - " \"messages\": [\n", - " (\n", - " \"user\",\n", - " f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\"\n", - " \"\\n\\nRemember to respond using the TranscriptSummary function.\",\n", - " )\n", - " ]\n", - " },\n", - ")\n", - "results.pretty_print()" - ] + "source": ["tools = [TranscriptSummary]\nbound_llm = bind_validator_with_retries(\n llm,\n tools=tools,\n)\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", \"Respond directly using the TranscriptSummary function.\"),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\nchain = prompt | bound_llm\n\nresults = chain.invoke(\n {\n \"messages\": [\n (\n \"user\",\n f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\"\n \"\\n\\nRemember to respond using the TranscriptSummary function.\",\n )\n ]\n },\n)\nresults.pretty_print()"] }, { "cell_type": "markdown", @@ -765,10 +233,7 @@ "id": "49344104-3ffa-4c66-97fc-5b093a621f70", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U jsonpatch" - ] + "source": ["%%capture --no-stderr\n%pip install -U jsonpatch"] }, { "cell_type": "code", @@ -776,150 +241,7 @@ "id": "af3d5543-1fd4-4e54-b0f9-f1ab42773cfb", "metadata": {}, "outputs": [], - "source": [ - "import logging\n", - "\n", - "logger = logging.getLogger(\"extraction\")\n", - "\n", - "\n", - "def bind_validator_with_jsonpatch_retries(\n", - " llm: BaseChatModel,\n", - " *,\n", - " tools: list,\n", - " tool_choice: Optional[str] = None,\n", - " max_attempts: int = 3,\n", - ") -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n", - " \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n", - "\n", - " This method is similar to `bind_validator_with_retries`, but uses JSONPatch to correct\n", - " validation errors caused by passing in incorrect or incomplete parameters in a previous\n", - " tool call. This method requires the 'jsonpatch' library to be installed.\n", - "\n", - " Using patch-based function healing can be more efficient than repopulating the entire\n", - " tool call from scratch, and it can be an easier task for the LLM to perform, since it typically\n", - " only requires a few small changes to the existing tool call.\n", - "\n", - " Args:\n", - " llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n", - " tools (list): The tools to bind to the LLM.\n", - " tool_choice (Optional[str]): The tool choice to use.\n", - " max_attempts (int): The number of attempts to make.\n", - "\n", - " Returns:\n", - " Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n", - " \"\"\"\n", - "\n", - " try:\n", - " import jsonpatch # type: ignore[import-untyped]\n", - " except ImportError:\n", - " raise ImportError(\n", - " \"The 'jsonpatch' library is required for JSONPatch-based retries.\"\n", - " \" Please install it with 'pip install -U jsonpatch'.\"\n", - " )\n", - "\n", - " class JsonPatch(BaseModel):\n", - " \"\"\"A JSON Patch document represents an operation to be performed on a JSON document.\n", - "\n", - " Note that the op and path are ALWAYS required. Value is required for ALL operations except 'remove'.\n", - " Examples:\n", - "\n", - " ```json\n", - " {\"op\": \"add\", \"path\": \"/a/b/c\", \"patch_value\": 1}\n", - " {\"op\": \"replace\", \"path\": \"/a/b/c\", \"patch_value\": 2}\n", - " {\"op\": \"remove\", \"path\": \"/a/b/c\"}\n", - " ```\n", - " \"\"\"\n", - "\n", - " op: Literal[\"add\", \"remove\", \"replace\"] = Field(\n", - " ...,\n", - " description=\"The operation to be performed. Must be one of 'add', 'remove', 'replace'.\",\n", - " )\n", - " path: str = Field(\n", - " ...,\n", - " description=\"A JSON Pointer path that references a location within the target document where the operation is performed.\",\n", - " )\n", - " value: Any = Field(\n", - " ...,\n", - " description=\"The value to be used within the operation. REQUIRED for 'add', 'replace', and 'test' operations.\",\n", - " )\n", - "\n", - " class PatchFunctionParameters(BaseModel):\n", - " \"\"\"Respond with all JSONPatch operation to correct validation errors caused by passing in incorrect or incomplete parameters in a previous tool call.\"\"\"\n", - "\n", - " tool_call_id: str = Field(\n", - " ...,\n", - " description=\"The ID of the original tool call that generated the error. Must NOT be an ID of a PatchFunctionParameters tool call.\",\n", - " )\n", - " reasoning: str = Field(\n", - " ...,\n", - " description=\"Think step-by-step, listing each validation error and the\"\n", - " \" JSONPatch operation needed to correct it. \"\n", - " \"Cite the fields in the JSONSchema you referenced in developing this plan.\",\n", - " )\n", - " patches: list[JsonPatch] = Field(\n", - " ...,\n", - " description=\"A list of JSONPatch operations to be applied to the previous tool call's response.\",\n", - " )\n", - "\n", - " bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n", - " fallback_llm = llm.bind_tools([PatchFunctionParameters])\n", - "\n", - " def aggregate_messages(messages: Sequence[AnyMessage]) -> AIMessage:\n", - " # Get all the AI messages and apply json patches\n", - " resolved_tool_calls: Dict[Union[str, None], ToolCall] = {}\n", - " content: Union[str, List[Union[str, dict]]] = \"\"\n", - " for m in messages:\n", - " if m.type != \"ai\":\n", - " continue\n", - " if not content:\n", - " content = m.content\n", - " for tc in m.tool_calls:\n", - " if tc[\"name\"] == PatchFunctionParameters.__name__:\n", - " tcid = tc[\"args\"][\"tool_call_id\"]\n", - " if tcid not in resolved_tool_calls:\n", - " logger.debug(\n", - " f\"JsonPatch tool call ID {tc['args']['tool_call_id']} not found.\"\n", - " f\"Valid tool call IDs: {list(resolved_tool_calls.keys())}\"\n", - " )\n", - " tcid = next(iter(resolved_tool_calls.keys()), None)\n", - " orig_tool_call = resolved_tool_calls[tcid]\n", - " current_args = orig_tool_call[\"args\"]\n", - " patches = tc[\"args\"].get(\"patches\") or []\n", - " orig_tool_call[\"args\"] = jsonpatch.apply_patch(\n", - " current_args,\n", - " patches,\n", - " )\n", - " orig_tool_call[\"id\"] = tc[\"id\"]\n", - " else:\n", - " resolved_tool_calls[tc[\"id\"]] = tc.copy()\n", - " return AIMessage(\n", - " content=content,\n", - " tool_calls=list(resolved_tool_calls.values()),\n", - " )\n", - "\n", - " def format_exception(error: BaseException, call: ToolCall, schema: Type[BaseModel]):\n", - " return (\n", - " f\"Error:\\n\\n```\\n{repr(error)}\\n```\\n\"\n", - " \"Expected Parameter Schema:\\n\\n\" + f\"```json\\n{schema.schema_json()}\\n```\\n\"\n", - " f\"Please respond with a JSONPatch to correct the error for tool_call_id=[{call['id']}].\"\n", - " )\n", - "\n", - " validator = ValidationNode(\n", - " tools + [PatchFunctionParameters],\n", - " format_error=format_exception,\n", - " )\n", - " retry_strategy = RetryStrategy(\n", - " max_attempts=max_attempts,\n", - " fallback=fallback_llm,\n", - " aggregate_messages=aggregate_messages,\n", - " )\n", - " return _bind_validator_with_retries(\n", - " bound_llm,\n", - " validator=validator,\n", - " retry_strategy=retry_strategy,\n", - " tool_choice=tool_choice,\n", - " ).with_config(metadata={\"retry_strategy\": \"jsonpatch\"})" - ] + "source": ["import logging\n\nlogger = logging.getLogger(\"extraction\")\n\n\ndef bind_validator_with_jsonpatch_retries(\n llm: BaseChatModel,\n *,\n tools: list,\n tool_choice: Optional[str] = None,\n max_attempts: int = 3,\n) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n\n This method is similar to `bind_validator_with_retries`, but uses JSONPatch to correct\n validation errors caused by passing in incorrect or incomplete parameters in a previous\n tool call. This method requires the 'jsonpatch' library to be installed.\n\n Using patch-based function healing can be more efficient than repopulating the entire\n tool call from scratch, and it can be an easier task for the LLM to perform, since it typically\n only requires a few small changes to the existing tool call.\n\n Args:\n llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n tools (list): The tools to bind to the LLM.\n tool_choice (Optional[str]): The tool choice to use.\n max_attempts (int): The number of attempts to make.\n\n Returns:\n Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n \"\"\"\n\n try:\n import jsonpatch # type: ignore[import-untyped]\n except ImportError:\n raise ImportError(\n \"The 'jsonpatch' library is required for JSONPatch-based retries.\"\n \" Please install it with 'pip install -U jsonpatch'.\"\n )\n\n class JsonPatch(BaseModel):\n \"\"\"A JSON Patch document represents an operation to be performed on a JSON document.\n\n Note that the op and path are ALWAYS required. Value is required for ALL operations except 'remove'.\n Examples:\n\n ```json\n {\"op\": \"add\", \"path\": \"/a/b/c\", \"patch_value\": 1}\n {\"op\": \"replace\", \"path\": \"/a/b/c\", \"patch_value\": 2}\n {\"op\": \"remove\", \"path\": \"/a/b/c\"}\n ```\n \"\"\"\n\n op: Literal[\"add\", \"remove\", \"replace\"] = Field(\n ...,\n description=\"The operation to be performed. Must be one of 'add', 'remove', 'replace'.\",\n )\n path: str = Field(\n ...,\n description=\"A JSON Pointer path that references a location within the target document where the operation is performed.\",\n )\n value: Any = Field(\n ...,\n description=\"The value to be used within the operation. REQUIRED for 'add', 'replace', and 'test' operations.\",\n )\n\n class PatchFunctionParameters(BaseModel):\n \"\"\"Respond with all JSONPatch operation to correct validation errors caused by passing in incorrect or incomplete parameters in a previous tool call.\"\"\"\n\n tool_call_id: str = Field(\n ...,\n description=\"The ID of the original tool call that generated the error. Must NOT be an ID of a PatchFunctionParameters tool call.\",\n )\n reasoning: str = Field(\n ...,\n description=\"Think step-by-step, listing each validation error and the\"\n \" JSONPatch operation needed to correct it. \"\n \"Cite the fields in the JSONSchema you referenced in developing this plan.\",\n )\n patches: list[JsonPatch] = Field(\n ...,\n description=\"A list of JSONPatch operations to be applied to the previous tool call's response.\",\n )\n\n bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n fallback_llm = llm.bind_tools([PatchFunctionParameters])\n\n def aggregate_messages(messages: Sequence[AnyMessage]) -> AIMessage:\n # Get all the AI messages and apply json patches\n resolved_tool_calls: Dict[Union[str, None], ToolCall] = {}\n content: Union[str, List[Union[str, dict]]] = \"\"\n for m in messages:\n if m.type != \"ai\":\n continue\n if not content:\n content = m.content\n for tc in m.tool_calls:\n if tc[\"name\"] == PatchFunctionParameters.__name__:\n tcid = tc[\"args\"][\"tool_call_id\"]\n if tcid not in resolved_tool_calls:\n logger.debug(\n f\"JsonPatch tool call ID {tc['args']['tool_call_id']} not found.\"\n f\"Valid tool call IDs: {list(resolved_tool_calls.keys())}\"\n )\n tcid = next(iter(resolved_tool_calls.keys()), None)\n orig_tool_call = resolved_tool_calls[tcid]\n current_args = orig_tool_call[\"args\"]\n patches = tc[\"args\"].get(\"patches\") or []\n orig_tool_call[\"args\"] = jsonpatch.apply_patch(\n current_args,\n patches,\n )\n orig_tool_call[\"id\"] = tc[\"id\"]\n else:\n resolved_tool_calls[tc[\"id\"]] = tc.copy()\n return AIMessage(\n content=content,\n tool_calls=list(resolved_tool_calls.values()),\n )\n\n def format_exception(error: BaseException, call: ToolCall, schema: Type[BaseModel]):\n return (\n f\"Error:\\n\\n```\\n{repr(error)}\\n```\\n\"\n \"Expected Parameter Schema:\\n\\n\" + f\"```json\\n{schema.schema_json()}\\n```\\n\"\n f\"Please respond with a JSONPatch to correct the error for tool_call_id=[{call['id']}].\"\n )\n\n validator = ValidationNode(\n tools + [PatchFunctionParameters],\n format_error=format_exception,\n )\n retry_strategy = RetryStrategy(\n max_attempts=max_attempts,\n fallback=fallback_llm,\n aggregate_messages=aggregate_messages,\n )\n return _bind_validator_with_retries(\n bound_llm,\n validator=validator,\n retry_strategy=retry_strategy,\n tool_choice=tool_choice,\n ).with_config(metadata={\"retry_strategy\": \"jsonpatch\"})"] }, { "cell_type": "code", @@ -927,9 +249,7 @@ "id": "b01891c4-4187-4a75-9eda-644a7c2355f3", "metadata": {}, "outputs": [], - "source": [ - "bound_llm = bind_validator_with_jsonpatch_retries(llm, tools=tools)" - ] + "source": ["bound_llm = bind_validator_with_jsonpatch_retries(llm, tools=tools)"] }, { "cell_type": "code", @@ -948,14 +268,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(bound_llm.get_graph().draw_mermaid_png()))\n", - "except Exception:\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(bound_llm.get_graph().draw_mermaid_png()))\nexcept Exception:\n pass"] }, { "cell_type": "code", @@ -984,20 +297,7 @@ ] } ], - "source": [ - "chain = prompt | bound_llm\n", - "results = chain.invoke(\n", - " {\n", - " \"messages\": [\n", - " (\n", - " \"user\",\n", - " f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\",\n", - " ),\n", - " ]\n", - " },\n", - ")\n", - "results.pretty_print()" - ] + "source": ["chain = prompt | bound_llm\nresults = chain.invoke(\n {\n \"messages\": [\n (\n \"user\",\n f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\",\n ),\n ]\n },\n)\nresults.pretty_print()"] }, { "cell_type": "markdown", @@ -1017,7 +317,7 @@ "id": "0ae295b1-da58-4cc9-834b-70e1466f8695", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/force-calling-a-tool-first.ipynb b/examples/force-calling-a-tool-first.ipynb index 123829bf2..3651af3f7 100644 --- a/examples/force-calling-a-tool-first.ipynb +++ b/examples/force-calling-a-tool-first.ipynb @@ -26,10 +26,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"] }, { "cell_type": "markdown", @@ -45,13 +42,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -67,10 +58,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -90,11 +78,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -112,11 +96,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -140,13 +120,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -164,9 +138,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -192,16 +164,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -236,66 +199,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation for each tool call\n", - " tool_invocations = []\n", - " for tool_call in last_message.tool_calls:\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " tool_invocations.append(action)\n", - "\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n", - " # We use the response to create tool messages\n", - " tool_messages = [\n", - " ToolMessage(\n", - " content=str(response),\n", - " name=tc[\"name\"],\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc, response in zip(last_message.tool_calls, responses)\n", - " ]\n", - "\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] }, { "cell_type": "markdown", @@ -313,30 +217,7 @@ "id": "1bfd2b22-292a-4f4d-91a0-46bb704f5e38", "metadata": {}, "outputs": [], - "source": [ - "# This is the new first - the first call of the model we want to explicitly hard-code some action\n", - "from langchain_core.messages import AIMessage\n", - "\n", - "\n", - "def first_model(state):\n", - " human_input = state[\"messages\"][-1].content\n", - " return {\n", - " \"messages\": [\n", - " AIMessage(\n", - " content=\"\",\n", - " tool_calls=[\n", - " {\n", - " \"name\": \"tavily_search_results_json\",\n", - " \"args\": {\n", - " \"query\": human_input,\n", - " },\n", - " \"id\": \"tool_abcd123\",\n", - " }\n", - " ],\n", - " )\n", - " ]\n", - " }" - ] + "source": ["# This is the new first - the first call of the model we want to explicitly hard-code some action\nfrom langchain_core.messages import AIMessage\n\n\ndef first_model(state):\n human_input = state[\"messages\"][-1].content\n return {\n \"messages\": [\n AIMessage(\n content=\"\",\n tool_calls=[\n {\n \"name\": \"tavily_search_results_json\",\n \"args\": {\n \"query\": human_input,\n },\n \"id\": \"tool_abcd123\",\n }\n ],\n )\n ]\n }"] }, { "cell_type": "markdown", @@ -358,56 +239,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the new entrypoint\n", - "workflow.add_node(\"first_agent\", first_model)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"first_agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# After we call the first agent, we know we want to go to action\n", - "workflow.add_edge(\"first_agent\", \"action\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the new entrypoint\nworkflow.add_node(\"first_agent\", first_model)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"first_agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After we call the first agent, we know we want to go to action\nworkflow.add_edge(\"first_agent\", \"action\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -426,15 +258,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -478,18 +302,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -497,7 +310,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/human-in-the-loop.ipynb b/examples/human-in-the-loop.ipynb index 76c05d588..0dd453a4c 100644 --- a/examples/human-in-the-loop.ipynb +++ b/examples/human-in-the-loop.ipynb @@ -39,10 +39,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"] }, { "cell_type": "markdown", @@ -58,18 +55,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -85,10 +71,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -106,22 +89,7 @@ "id": "6098e5cb", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# `add_messages`` essentially does this\n", - "# (with more robust handling)\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -141,22 +109,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\n", - " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", - " ]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -174,11 +127,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -199,11 +148,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -221,9 +166,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -258,53 +201,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " tool_call = last_message.tool_calls[0]\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a ToolMessage\n", - " tool_message = ToolMessage(\n", - " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", - " )\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -322,45 +219,7 @@ "id": "812b4e70-4956-4415-8880-db48b3dcbad2", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"] }, { "cell_type": "markdown", @@ -378,11 +237,7 @@ "id": "6845ed6a-d155-4105-9160-28849877248b", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "markdown", @@ -400,12 +255,7 @@ "id": "79d29875-8aa8-434c-9f20-1c58346a6249", "metadata": {}, "outputs": [], - "source": [ - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" - ] + "source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] }, { "cell_type": "markdown", @@ -432,11 +282,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -467,14 +313,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "thread = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "inputs = [HumanMessage(content=\"hi! I'm bob\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"2\"}}\ninputs = [HumanMessage(content=\"hi! I'm bob\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -495,11 +334,7 @@ ] } ], - "source": [ - "inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -523,11 +358,7 @@ ] } ], - "source": [ - "inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -561,10 +392,7 @@ ] } ], - "source": [ - "for event in app.stream(None, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -599,43 +427,7 @@ "id": "5454f436-d56e-4499-9381-06192aca1b56", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "from typing import Optional\n", - "\n", - "from langchain_core.messages import AIMessage\n", - "\n", - "\n", - "# Helper function to construct message asking for verification\n", - "def generate_verification_message(message: AIMessage) -> None:\n", - " \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n", - " serialized_tool_calls = json.dumps(\n", - " message.tool_calls,\n", - " indent=2,\n", - " )\n", - " return AIMessage(\n", - " content=(\n", - " \"I plan to invoke the following tools, do you approve?\\n\\n\"\n", - " \"Type 'y' if you do, anything else to stop.\\n\\n\"\n", - " f\"{serialized_tool_calls}\"\n", - " ),\n", - " id=message.id,\n", - " )\n", - "\n", - "\n", - "# Helper function to stream output from the graph\n", - "def stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n", - " \"\"\"Stream app, catching tool calls.\"\"\"\n", - " tool_call_message = None\n", - " for event in app.stream(inputs, thread, stream_mode=\"values\"):\n", - " message = event[\"messages\"][-1]\n", - " if isinstance(message, AIMessage) and message.tool_calls:\n", - " tool_call_message = message\n", - " else:\n", - " message.pretty_print()\n", - "\n", - " return tool_call_message" - ] + "source": ["import json\nfrom typing import Optional\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to construct message asking for verification\ndef generate_verification_message(message: AIMessage) -> None:\n \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n serialized_tool_calls = json.dumps(\n message.tool_calls,\n indent=2,\n )\n return AIMessage(\n content=(\n \"I plan to invoke the following tools, do you approve?\\n\\n\"\n \"Type 'y' if you do, anything else to stop.\\n\\n\"\n f\"{serialized_tool_calls}\"\n ),\n id=message.id,\n )\n\n\n# Helper function to stream output from the graph\ndef stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n \"\"\"Stream app, catching tool calls.\"\"\"\n tool_call_message = None\n for event in app.stream(inputs, thread, stream_mode=\"values\"):\n message = event[\"messages\"][-1]\n if isinstance(message, AIMessage) and message.tool_calls:\n tool_call_message = message\n else:\n message.pretty_print()\n\n return tool_call_message"] }, { "cell_type": "code", @@ -722,43 +514,7 @@ ] } ], - "source": [ - "import uuid\n", - "\n", - "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n", - "\n", - "tool_call_message = stream_app_catch_tool_calls(\n", - " {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n", - " thread,\n", - ")\n", - "\n", - "while tool_call_message:\n", - " verification_message = generate_verification_message(tool_call_message)\n", - " verification_message.pretty_print()\n", - " input_message = HumanMessage(input())\n", - " if input_message.content == \"exit\":\n", - " break\n", - " input_message.pretty_print()\n", - "\n", - " # First we update the state with the verification message and the input message.\n", - " # note that `generate_verification_message` sets the message ID to be the same\n", - " # as the ID from the original tool call message. Updating the state with this\n", - " # message will overwrite the previous tool call.\n", - " snapshot = app.get_state(thread)\n", - " snapshot.values[\"messages\"] += [verification_message, input_message]\n", - "\n", - " if input_message.content == \"y\":\n", - " tool_call_message.id = str(uuid.uuid4())\n", - " # If verified, we append the tool call message to the state\n", - " # and resume execution.\n", - " snapshot.values[\"messages\"] += [tool_call_message]\n", - " app.update_state(thread, snapshot.values, as_node=\"agent\")\n", - " else:\n", - " # Otherwise, resume execution from the input message.\n", - " app.update_state(thread, snapshot.values, as_node=\"__start__\")\n", - "\n", - " tool_call_message = stream_app_catch_tool_calls(None, thread)" - ] + "source": ["import uuid\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\n\ntool_call_message = stream_app_catch_tool_calls(\n {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n thread,\n)\n\nwhile tool_call_message:\n verification_message = generate_verification_message(tool_call_message)\n verification_message.pretty_print()\n input_message = HumanMessage(input())\n if input_message.content == \"exit\":\n break\n input_message.pretty_print()\n\n # First we update the state with the verification message and the input message.\n # note that `generate_verification_message` sets the message ID to be the same\n # as the ID from the original tool call message. Updating the state with this\n # message will overwrite the previous tool call.\n snapshot = app.get_state(thread)\n snapshot.values[\"messages\"] += [verification_message, input_message]\n\n if input_message.content == \"y\":\n tool_call_message.id = str(uuid.uuid4())\n # If verified, we append the tool call message to the state\n # and resume execution.\n snapshot.values[\"messages\"] += [tool_call_message]\n app.update_state(thread, snapshot.values, as_node=\"agent\")\n else:\n # Otherwise, resume execution from the input message.\n app.update_state(thread, snapshot.values, as_node=\"__start__\")\n\n tool_call_message = stream_app_catch_tool_calls(None, thread)"] }, { "cell_type": "markdown", @@ -779,34 +535,7 @@ "id": "03232f16-d6fe-46d0-afa0-a6f0d0bf16de", "metadata": {}, "outputs": [], - "source": [ - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - " tool_call_message: Optional[AIMessage]\n", - "\n", - "\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " if messages[-1].content == \"y\":\n", - " return {\n", - " \"messages\": [state[\"tool_call_message\"]],\n", - " \"tool_call_message\": None,\n", - " }\n", - " else:\n", - " response = model.invoke(messages)\n", - " if response.tool_calls:\n", - " verification_message = generate_verification_message(response)\n", - " response.id = str(uuid.uuid4())\n", - " return {\n", - " \"messages\": [verification_message],\n", - " \"tool_call_message\": response,\n", - " }\n", - " else:\n", - " return {\n", - " \"messages\": [response],\n", - " \"tool_call_message\": None,\n", - " }" - ] + "source": ["class State(TypedDict):\n messages: Annotated[list, add_messages]\n tool_call_message: Optional[AIMessage]\n\n\ndef call_model(state):\n messages = state[\"messages\"]\n if messages[-1].content == \"y\":\n return {\n \"messages\": [state[\"tool_call_message\"]],\n \"tool_call_message\": None,\n }\n else:\n response = model.invoke(messages)\n if response.tool_calls:\n verification_message = generate_verification_message(response)\n response.id = str(uuid.uuid4())\n return {\n \"messages\": [verification_message],\n \"tool_call_message\": response,\n }\n else:\n return {\n \"messages\": [response],\n \"tool_call_message\": None,\n }"] }, { "cell_type": "markdown", @@ -822,27 +551,7 @@ "id": "502dc688-c926-407e-8759-8c9e39eb4257", "metadata": {}, "outputs": [], - "source": [ - "workflow = StateGraph(State)\n", - "\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - " {\n", - " \"continue\": \"action\",\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "app = workflow.compile(checkpointer=memory)" - ] + "source": ["workflow = StateGraph(State)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\nworkflow.add_edge(START, \"agent\")\n\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n {\n \"continue\": \"action\",\n \"end\": END,\n },\n)\n\nworkflow.add_edge(\"action\", \"agent\")\n\napp = workflow.compile(checkpointer=memory)"] }, { "cell_type": "code", @@ -875,13 +584,7 @@ ] } ], - "source": [ - "thread = {\"configurable\": {\"thread_id\": \"4\"}}\n", - "\n", - "inputs = [HumanMessage(content=\"what's the weather in sf?\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["thread = {\"configurable\": {\"thread_id\": \"4\"}}\n\ninputs = [HumanMessage(content=\"what's the weather in sf?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -914,11 +617,7 @@ ] } ], - "source": [ - "inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -949,11 +648,7 @@ ] } ], - "source": [ - "inputs = [HumanMessage(content=\"y\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["inputs = [HumanMessage(content=\"y\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] } ], "metadata": { diff --git a/examples/human_in_the_loop/breakpoints.ipynb b/examples/human_in_the_loop/breakpoints.ipynb index 61a26fc17..868ea9b20 100644 --- a/examples/human_in_the_loop/breakpoints.ipynb +++ b/examples/human_in_the_loop/breakpoints.ipynb @@ -32,10 +32,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] }, { "cell_type": "markdown", @@ -59,18 +56,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -86,10 +72,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -107,103 +90,7 @@ "id": "6098e5cb", "metadata": {}, "outputs": [], - "source": [ - "# Set up the tool\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.tools import tool\n", - "from langgraph.graph import MessagesState\n", - "from langgraph.prebuilt import ToolNode\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\n", - " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", - " ]\n", - "\n", - "tools = [search]\n", - "tool_node = ToolNode(tools)\n", - "\n", - "# Set up the model\n", - "\n", - "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", - "model = model.bind_tools(tools)\n", - "\n", - "\n", - "# Define nodes and conditional edges\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(MessagesState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Set up memory\n", - "memory = MemorySaver()\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "\n", - "# We add in `interrupt_before=[\"action\"]`\n", - "# This will add a breakpoint before the `action` node is called\n", - "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" - ] + "source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] }, { "cell_type": "markdown", @@ -239,14 +126,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n", - "inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -286,10 +166,7 @@ ] } ], - "source": [ - "for event in app.stream(None, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] } ], "metadata": { diff --git a/examples/human_in_the_loop/edit-graph-state.ipynb b/examples/human_in_the_loop/edit-graph-state.ipynb index 6a8585cf8..145940a33 100644 --- a/examples/human_in_the_loop/edit-graph-state.ipynb +++ b/examples/human_in_the_loop/edit-graph-state.ipynb @@ -32,10 +32,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] }, { "cell_type": "markdown", @@ -59,18 +56,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -86,10 +72,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -107,103 +90,7 @@ "id": "6098e5cb", "metadata": {}, "outputs": [], - "source": [ - "# Set up the tool\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.tools import tool\n", - "from langgraph.graph import MessagesState\n", - "from langgraph.prebuilt import ToolNode\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\n", - " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", - " ]\n", - "\n", - "tools = [search]\n", - "tool_node = ToolNode(tools)\n", - "\n", - "# Set up the model\n", - "\n", - "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", - "model = model.bind_tools(tools)\n", - "\n", - "\n", - "# Define nodes and conditional edges\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(MessagesState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Set up memory\n", - "memory = MemorySaver()\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "\n", - "# We add in `interrupt_before=[\"action\"]`\n", - "# This will add a breakpoint before the `action` node is called\n", - "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" - ] + "source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] }, { "cell_type": "markdown", @@ -239,14 +126,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n", - "inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n", - "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -276,25 +156,7 @@ "output_type": "execute_result" } ], - "source": [ - "# First, lets get the current state\n", - "current_state = app.get_state(thread)\n", - "\n", - "# Let's now get the last message in the state\n", - "# This is the one with the tool calls that we want to update\n", - "last_message = current_state.values['messages'][-1]\n", - "\n", - "# Let's now update the args for that tool call\n", - "last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n", - "\n", - "# Let's now call `update_state` to pass in this message in the `messages` key\n", - "# This will get treated as any other update to the state\n", - "# It will get passed to the reducer function for the `messages` key\n", - "# That reducer function will use the ID of the message to update it\n", - "# It's important that it has the right ID! Otherwise it would get appended\n", - "# as a new message\n", - "app.update_state(thread, {\"messages\": last_message})" - ] + "source": ["# First, lets get the current state\ncurrent_state = app.get_state(thread)\n\n# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = current_state.values['messages'][-1]\n\n# Let's now update the args for that tool call\nlast_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n\n# Let's now call `update_state` to pass in this message in the `messages` key\n# This will get treated as any other update to the state\n# It will get passed to the reducer function for the `messages` key\n# That reducer function will use the ID of the message to update it\n# It's important that it has the right ID! Otherwise it would get appended\n# as a new message\napp.update_state(thread, {\"messages\": last_message})"] }, { "cell_type": "markdown", @@ -323,10 +185,7 @@ "output_type": "execute_result" } ], - "source": [ - "current_state = app.get_state(thread).values['messages'][-1].tool_calls\n", - "current_state" - ] + "source": ["current_state = app.get_state(thread).values['messages'][-1].tool_calls\ncurrent_state"] }, { "cell_type": "markdown", @@ -364,10 +223,7 @@ ] } ], - "source": [ - "for event in app.stream(None, thread, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -375,7 +231,7 @@ "id": "78780afe-409d-46cd-a734-e82538cdd8de", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/human_in_the_loop/time-travel.ipynb b/examples/human_in_the_loop/time-travel.ipynb index 9650c023e..04c89eec9 100644 --- a/examples/human_in_the_loop/time-travel.ipynb +++ b/examples/human_in_the_loop/time-travel.ipynb @@ -39,10 +39,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] }, { "cell_type": "markdown", @@ -66,18 +63,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -93,10 +79,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -114,103 +97,7 @@ "id": "f5319e01", "metadata": {}, "outputs": [], - "source": [ - "# Set up the tool\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.tools import tool\n", - "from langgraph.graph import MessagesState\n", - "from langgraph.prebuilt import ToolNode\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\n", - " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", - " ]\n", - "\n", - "tools = [search]\n", - "tool_node = ToolNode(tools)\n", - "\n", - "# Set up the model\n", - "\n", - "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", - "model = model.bind_tools(tools)\n", - "\n", - "\n", - "# Define nodes and conditional edges\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(MessagesState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Set up memory\n", - "memory = MemorySaver()\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "\n", - "# We add in `interrupt_before=[\"action\"]`\n", - "# This will add a breakpoint before the `action` node is called\n", - "app = workflow.compile(checkpointer=memory)" - ] + "source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory)"] }, { "cell_type": "markdown", @@ -264,14 +151,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "input_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\ninput_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -306,13 +186,7 @@ ] } ], - "source": [ - "all_states = []\n", - "for state in app.get_state_history(config):\n", - " print(state)\n", - " all_states.append(state)\n", - " print(\"--\")" - ] + "source": ["all_states = []\nfor state in app.get_state_history(config):\n print(state)\n all_states.append(state)\n print(\"--\")"] }, { "cell_type": "markdown", @@ -330,9 +204,7 @@ "id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699", "metadata": {}, "outputs": [], - "source": [ - "to_replay = all_states[2]" - ] + "source": ["to_replay = all_states[2]"] }, { "cell_type": "code", @@ -352,9 +224,7 @@ "output_type": "execute_result" } ], - "source": [ - "to_replay.values" - ] + "source": ["to_replay.values"] }, { "cell_type": "code", @@ -373,9 +243,7 @@ "output_type": "execute_result" } ], - "source": [ - "to_replay.next" - ] + "source": ["to_replay.next"] }, { "cell_type": "markdown", @@ -400,11 +268,7 @@ ] } ], - "source": [ - "for event in app.stream(None, to_replay.config):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["for event in app.stream(None, to_replay.config):\n for v in event.values():\n print(v)"] }, { "cell_type": "markdown", @@ -424,18 +288,7 @@ "id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f", "metadata": {}, "outputs": [], - "source": [ - "# Let's now get the last message in the state\n", - "# This is the one with the tool calls that we want to update\n", - "last_message = to_replay.values['messages'][-1]\n", - "\n", - "# Let's now update the args for that tool call\n", - "last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n", - "\n", - "branch_config = app.update_state(\n", - " to_replay.config, {\"messages\": [last_message]},\n", - ")" - ] + "source": ["# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = to_replay.values['messages'][-1]\n\n# Let's now update the args for that tool call\nlast_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n\nbranch_config = app.update_state(\n to_replay.config, {\"messages\": [last_message]},\n)"] }, { "cell_type": "markdown", @@ -460,11 +313,7 @@ ] } ], - "source": [ - "for event in app.stream(None, branch_config):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["for event in app.stream(None, branch_config):\n for v in event.values():\n print(v)"] }, { "cell_type": "markdown", @@ -480,20 +329,7 @@ "id": "01abb480-df55-4eba-a2be-cf9372b60b54", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "# Let's now get the last message in the state\n", - "# This is the one with the tool calls that we want to update\n", - "last_message = to_replay.values['messages'][-1]\n", - "\n", - "# Let's now get the ID for the last message, and create a new message with that ID.\n", - "new_message = AIMessage(content=\"its warm!\", id=last_message.id)\n", - "\n", - "branch_config = app.update_state(\n", - " to_replay.config, {\"messages\": [new_message]},\n", - ")" - ] + "source": ["from langchain_core.messages import AIMessage\n\n# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = to_replay.values['messages'][-1]\n\n# Let's now get the ID for the last message, and create a new message with that ID.\nnew_message = AIMessage(content=\"its warm!\", id=last_message.id)\n\nbranch_config = app.update_state(\n to_replay.config, {\"messages\": [new_message]},\n)"] }, { "cell_type": "code", @@ -501,9 +337,7 @@ "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", "metadata": {}, "outputs": [], - "source": [ - "branch_state = app.get_state(branch_config)" - ] + "source": ["branch_state = app.get_state(branch_config)"] }, { "cell_type": "code", @@ -523,9 +357,7 @@ "output_type": "execute_result" } ], - "source": [ - "branch_state.values" - ] + "source": ["branch_state.values"] }, { "cell_type": "code", @@ -544,9 +376,7 @@ "output_type": "execute_result" } ], - "source": [ - "branch_state.next" - ] + "source": ["branch_state.next"] }, { "cell_type": "markdown", @@ -562,7 +392,7 @@ "id": "74a7a5ed-0c14-4883-a16b-d70aaf40f7ea", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb index dc6d0a5c1..494505a5b 100644 --- a/examples/human_in_the_loop/wait-user-input.ipynb +++ b/examples/human_in_the_loop/wait-user-input.ipynb @@ -40,10 +40,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] }, { "cell_type": "markdown", @@ -67,18 +64,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -94,10 +80,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -115,144 +98,7 @@ "id": "f5319e01", "metadata": {}, "outputs": [], - "source": [ - "# Set up the state\n", - "from langgraph.graph import MessagesState\n", - "\n", - "# Set up the tool\n", - "# We will have one real tool - a search tool\n", - "# We'll also have one \"fake\" tool - a \"ask_human\" tool\n", - "# Here we define any ACTUAL tools\n", - "from langchain_core.tools import tool\n", - "from langgraph.prebuilt import ToolNode\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\n", - " f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", - " ]\n", - "\n", - "\n", - "tools = [search]\n", - "tool_node = ToolNode(tools)\n", - "\n", - "# Set up the model\n", - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", - "\n", - "\n", - "# We are going \"bind\" all tools to the model\n", - "# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n", - "# Since `bind_tools` takes in tools but also just tool definitions,\n", - "# We can define a tool definition for `ask_human`\n", - "\n", - "from langchain_core.pydantic_v1 import BaseModel\n", - "\n", - "class AskHuman(BaseModel):\n", - " \"\"\"Ask the human a question\"\"\"\n", - " question: str\n", - "\n", - "\n", - "model = model.bind_tools(tools + [AskHuman])\n", - "\n", - "# Define nodes and conditional edges\n", - "\n", - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # If tool call is asking Human, we return that node\n", - " # You could also add logic here to let some system know that there's something that requires Human input\n", - " # For example, send a slack message, etc\n", - " elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n", - " return \"ask_human\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "\n", - "# We define a fake node to ask the human\n", - "def ask_human(state):\n", - " pass\n", - "\n", - "# Build the graph\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(MessagesState)\n", - "\n", - "# Define the three nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "workflow.add_node(\"ask_human\", ask_human)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # We may ask the human\n", - " \"ask_human\": \"ask_human\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# After we get back the human response, we go back to the agent\n", - "workflow.add_edge(\"ask_human\", \"agent\")\n", - "\n", - "# Set up memory\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "memory = MemorySaver()\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "# We add a breakpoint BEFORE the `ask_human` node so it never executes\n", - "app = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])" - ] + "source": ["# Set up the state\nfrom langgraph.graph import MessagesState, START\n\n# Set up the tool\n# We will have one real tool - a search tool\n# We'll also have one \"fake\" tool - a \"ask_human\" tool\n# Here we define any ACTUAL tools\nfrom langchain_core.tools import tool\nfrom langgraph.prebuilt import ToolNode\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\nfrom langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n\n\n# We are going \"bind\" all tools to the model\n# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n# Since `bind_tools` takes in tools but also just tool definitions,\n# We can define a tool definition for `ask_human`\n\nfrom langchain_core.pydantic_v1 import BaseModel\n\nclass AskHuman(BaseModel):\n \"\"\"Ask the human a question\"\"\"\n question: str\n\n\nmodel = model.bind_tools(tools + [AskHuman])\n\n# Define nodes and conditional edges\n\nfrom langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # If tool call is asking Human, we return that node\n # You could also add logic here to let some system know that there's something that requires Human input\n # For example, send a slack message, etc\n elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n return \"ask_human\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n\n# We define a fake node to ask the human\ndef ask_human(state):\n pass\n\n# Build the graph\n\nfrom langgraph.graph import END, StateGraph\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the three nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\nworkflow.add_node(\"ask_human\", ask_human)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # We may ask the human\n \"ask_human\": \"ask_human\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After we get back the human response, we go back to the agent\nworkflow.add_edge(\"ask_human\", \"agent\")\n\n# Set up memory\nfrom langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n# We add a breakpoint BEFORE the `ask_human` node so it never executes\napp = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])"] }, { "cell_type": "markdown", @@ -288,14 +134,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "input_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -324,29 +163,7 @@ "output_type": "execute_result" } ], - "source": [ - "tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n", - "\n", - "# We now create the tool call with the id and the response we want\n", - "tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n", - "\n", - "# # This is equivalent to the below, either one works\n", - "# from langchain_core.messages import ToolMessage\n", - "# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n", - "\n", - "# We now update the state\n", - "# Notice that we are also specifying `as_node=\"ask_human\"`\n", - "# This will apply this update as this node,\n", - "# which will make it so that afterwards it continues as normal\n", - "app.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n", - "\n", - "# We can check the state\n", - "# We can see that the state currently has the `agent` node next\n", - "# This is based on how we define our graph, \n", - "# where after the `ask_human` node goes (which we just triggered)\n", - "# there is an edge to the `agent` node\n", - "app.get_state(config).next" - ] + "source": ["tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n\n# We now create the tool call with the id and the response we want\ntool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n\n# # This is equivalent to the below, either one works\n# from langchain_core.messages import ToolMessage\n# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n\n# We now update the state\n# Notice that we are also specifying `as_node=\"ask_human\"`\n# This will apply this update as this node,\n# which will make it so that afterwards it continues as normal\napp.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n\n# We can check the state\n# We can see that the state currently has the `agent` node next\n# This is based on how we define our graph, \n# where after the `ask_human` node goes (which we just triggered)\n# there is an edge to the `agent` node\napp.get_state(config).next"] }, { "cell_type": "markdown", @@ -388,10 +205,7 @@ ] } ], - "source": [ - "for event in app.stream(None, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["for event in app.stream(None, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -399,7 +213,7 @@ "id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index d3d27e14d..bd4319b65 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -28,13 +28,7 @@ "id": "6f11d631-8679-4f28-822f-cdf1f2ddc21c", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langsmith\n", - "\n", - "# Used for this tutorial; not a requirement for LangGraph\n", - "%pip install -U langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph langsmith\n\n# Used for this tutorial; not a requirement for LangGraph\n%pip install -U langchain_anthropic"] }, { "cell_type": "markdown", @@ -50,18 +44,7 @@ "id": "705d4020-6ee8-44cc-b1a5-8c34e7172fc7", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -77,11 +60,7 @@ "id": "13cba9af-0572-41df-92f8-d6f56d5b5322", "metadata": {}, "outputs": [], - "source": [ - "_set_env(\"LANGSMITH_API_KEY\")\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"LangGraph Tutorial\"" - ] + "source": ["_set_env(\"LANGSMITH_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"LangGraph Tutorial\""] }, { "cell_type": "markdown", @@ -101,24 +80,7 @@ "id": "e58df974-7579-4f25-9d91-66389b94eba2", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # Messages have the type \"list\". The `add_messages` function\n", - " # in the annotation defines how this state key should be updated\n", - " # (in this case, it appends messages to the list, rather than overwriting them)\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "graph_builder = StateGraph(State)" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n # Messages have the type \"list\". The `add_messages` function\n # in the annotation defines how this state key should be updated\n # (in this case, it appends messages to the list, rather than overwriting them)\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)"] }, { "cell_type": "markdown", @@ -141,21 +103,7 @@ "id": "bc8c9137-8261-42ea-8e83-3590981d23e2", "metadata": {}, "outputs": [], - "source": [ - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " return {\"messages\": [llm.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "# The first argument is the unique node name\n", - "# The second argument is the function or object that will be called whenever\n", - "# the node is used.\n", - "graph_builder.add_node(\"chatbot\", chatbot)" - ] + "source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm.invoke(state[\"messages\"])]}\n\n\n# The first argument is the unique node name\n# The second argument is the function or object that will be called whenever\n# the node is used.\ngraph_builder.add_node(\"chatbot\", chatbot)"] }, { "cell_type": "markdown", @@ -175,9 +123,7 @@ "id": "e331e10d-ebcf-4144-9bd3-999b4d656dd3", "metadata": {}, "outputs": [], - "source": [ - "graph_builder.set_entry_point(\"chatbot\")" - ] + "source": ["graph_builder.add_edge(START, \"chatbot\")"] }, { "cell_type": "markdown", @@ -193,9 +139,7 @@ "id": "075f0929-3591-4852-b2d3-eaadde40662d", "metadata": {}, "outputs": [], - "source": [ - "graph_builder.set_finish_point(\"chatbot\")" - ] + "source": ["graph_builder.set_finish_point(\"chatbot\")"] }, { "cell_type": "markdown", @@ -211,9 +155,7 @@ "id": "0bb67a01-cf5c-4625-8c07-6e8c0af50fca", "metadata": {}, "outputs": [], - "source": [ - "graph = graph_builder.compile()" - ] + "source": ["graph = graph_builder.compile()"] }, { "cell_type": "markdown", @@ -240,15 +182,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -323,16 +257,7 @@ ] } ], - "source": [ - "while True:\n", - " user_input = input(\"User: \")\n", - " if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n", - " print(\"Goodbye!\")\n", - " break\n", - " for event in graph.stream({\"messages\": (\"user\", user_input)}):\n", - " for value in event.values():\n", - " print(\"Assistant:\", value[\"messages\"][-1].content)" - ] + "source": ["while True:\n user_input = input(\"User: \")\n if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n print(\"Goodbye!\")\n break\n for event in graph.stream({\"messages\": (\"user\", user_input)}):\n for value in event.values():\n print(\"Assistant:\", value[\"messages\"][-1].content)"] }, { "cell_type": "markdown", @@ -408,11 +333,7 @@ "id": "7451151f-41fc-4af0-9359-024ae51b7225", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U tavily-python\n", - "%pip install -U langchain_community" - ] + "source": ["%%capture --no-stderr\n%pip install -U tavily-python\n%pip install -U langchain_community"] }, { "cell_type": "code", @@ -420,9 +341,7 @@ "id": "0c52923c-5665-4f8c-a1ba-9799e369c49e", "metadata": {}, "outputs": [], - "source": [ - "_set_env(\"TAVILY_API_KEY\")" - ] + "source": ["_set_env(\"TAVILY_API_KEY\")"] }, { "cell_type": "markdown", @@ -452,13 +371,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "tool.invoke(\"What's a 'node' in LangGraph?\")" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\ntool.invoke(\"What's a 'node' in LangGraph?\")"] }, { "cell_type": "markdown", @@ -477,34 +390,7 @@ "id": "dc5af88b-47d2-43bf-9a2c-6c07506b1732", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "graph_builder = StateGraph(State)\n", - "\n", - "\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "# Modification: tell the LLM which tools it can call\n", - "llm_with_tools = llm.bind_tools(tools)\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n# Modification: tell the LLM which tools it can call\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)"] }, { "cell_type": "markdown", @@ -524,41 +410,7 @@ "id": "12f1fc14-cd91-4cd4-9f2e-1d007f8beafc", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "\n", - "from langchain_core.messages import ToolMessage\n", - "\n", - "\n", - "class BasicToolNode:\n", - " \"\"\"A node that runs the tools requested in the last AIMessage.\"\"\"\n", - "\n", - " def __init__(self, tools: list) -> None:\n", - " self.tools_by_name = {tool.name: tool for tool in tools}\n", - "\n", - " def __call__(self, inputs: dict):\n", - " if messages := inputs.get(\"messages\", []):\n", - " message = messages[-1]\n", - " else:\n", - " raise ValueError(\"No message found in input\")\n", - " outputs = []\n", - " for tool_call in message.tool_calls:\n", - " tool_result = self.tools_by_name[tool_call[\"name\"]].invoke(\n", - " tool_call[\"args\"]\n", - " )\n", - " outputs.append(\n", - " ToolMessage(\n", - " content=json.dumps(tool_result),\n", - " name=tool_call[\"name\"],\n", - " tool_call_id=tool_call[\"id\"],\n", - " )\n", - " )\n", - " return {\"messages\": outputs}\n", - "\n", - "\n", - "tool_node = BasicToolNode(tools=[tool])\n", - "graph_builder.add_node(\"tools\", tool_node)" - ] + "source": ["import json\n\nfrom langchain_core.messages import ToolMessage\n\n\nclass BasicToolNode:\n \"\"\"A node that runs the tools requested in the last AIMessage.\"\"\"\n\n def __init__(self, tools: list) -> None:\n self.tools_by_name = {tool.name: tool for tool in tools}\n\n def __call__(self, inputs: dict):\n if messages := inputs.get(\"messages\", []):\n message = messages[-1]\n else:\n raise ValueError(\"No message found in input\")\n outputs = []\n for tool_call in message.tool_calls:\n tool_result = self.tools_by_name[tool_call[\"name\"]].invoke(\n tool_call[\"args\"]\n )\n outputs.append(\n ToolMessage(\n content=json.dumps(tool_result),\n name=tool_call[\"name\"],\n tool_call_id=tool_call[\"id\"],\n )\n )\n return {\"messages\": outputs}\n\n\ntool_node = BasicToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)"] }, { "cell_type": "markdown", @@ -582,45 +434,7 @@ "id": "d662df94-66ac-4c6c-92f0-4c93620f1c74", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "\n", - "def route_tools(\n", - " state: State,\n", - ") -> Literal[\"tools\", \"__end__\"]:\n", - " \"\"\"\n", - " Use in the conditional_edge to route to the ToolNode if the last message\n", - " has tool calls. Otherwise, route to the end.\n", - " \"\"\"\n", - " if isinstance(state, list):\n", - " ai_message = state[-1]\n", - " elif messages := state.get(\"messages\", []):\n", - " ai_message = messages[-1]\n", - " else:\n", - " raise ValueError(f\"No messages found in input state to tool_edge: {state}\")\n", - " if hasattr(ai_message, \"tool_calls\") and len(ai_message.tool_calls) > 0:\n", - " return \"tools\"\n", - " return \"__end__\"\n", - "\n", - "\n", - "# The `tools_condition` function returns \"tools\" if the chatbot asks to use a tool, and \"__end__\" if\n", - "# it is fine directly responding. This conditional routing defines the main agent loop.\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " route_tools,\n", - " # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node\n", - " # It defaults to the identity function, but if you\n", - " # want to use a node named something else apart from \"tools\",\n", - " # You can update the value of the dictionary to something else\n", - " # e.g., \"tools\": \"my_tools\"\n", - " {\"tools\": \"tools\", \"__end__\": \"__end__\"},\n", - ")\n", - "# Any time a tool is called, we return to the chatbot to decide the next step\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.set_entry_point(\"chatbot\")\n", - "graph = graph_builder.compile()" - ] + "source": ["from typing import Literal\n\n\ndef route_tools(\n state: State,\n) -> Literal[\"tools\", \"__end__\"]:\n \"\"\"\n Use in the conditional_edge to route to the ToolNode if the last message\n has tool calls. Otherwise, route to the end.\n \"\"\"\n if isinstance(state, list):\n ai_message = state[-1]\n elif messages := state.get(\"messages\", []):\n ai_message = messages[-1]\n else:\n raise ValueError(f\"No messages found in input state to tool_edge: {state}\")\n if hasattr(ai_message, \"tool_calls\") and len(ai_message.tool_calls) > 0:\n return \"tools\"\n return \"__end__\"\n\n\n# The `tools_condition` function returns \"tools\" if the chatbot asks to use a tool, and \"__end__\" if\n# it is fine directly responding. This conditional routing defines the main agent loop.\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n route_tools,\n # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node\n # It defaults to the identity function, but if you\n # want to use a node named something else apart from \"tools\",\n # You can update the value of the dictionary to something else\n # e.g., \"tools\": \"my_tools\"\n {\"tools\": \"tools\", \"__end__\": \"__end__\"},\n)\n# Any time a tool is called, we return to the chatbot to decide the next step\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\ngraph = graph_builder.compile()"] }, { "cell_type": "markdown", @@ -651,15 +465,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -744,19 +550,7 @@ ] } ], - "source": [ - "from langchain_core.messages import BaseMessage\n", - "\n", - "while True:\n", - " user_input = input(\"User: \")\n", - " if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n", - " print(\"Goodbye!\")\n", - " break\n", - " for event in graph.stream({\"messages\": [(\"user\", user_input)]}):\n", - " for value in event.values():\n", - " if isinstance(value[\"messages\"][-1], BaseMessage):\n", - " print(\"Assistant:\", value[\"messages\"][-1].content)" - ] + "source": ["from langchain_core.messages import BaseMessage\n\nwhile True:\n user_input = input(\"User: \")\n if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n print(\"Goodbye!\")\n break\n for event in graph.stream({\"messages\": [(\"user\", user_input)]}):\n for value in event.values():\n if isinstance(value[\"messages\"][-1], BaseMessage):\n print(\"Assistant:\", value[\"messages\"][-1].content)"] }, { "cell_type": "markdown", @@ -845,11 +639,7 @@ "id": "6baafdf6-6803-4305-9381-9dc970468a4d", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "markdown", @@ -876,49 +666,7 @@ ] } ], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode, tools_condition\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "graph_builder = StateGraph(State)\n", - "\n", - "\n", - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm_with_tools = llm.bind_tools(tools)\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)\n", - "\n", - "tool_node = ToolNode(tools=[tool])\n", - "graph_builder.add_node(\"tools\", tool_node)\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " tools_condition,\n", - ")\n", - "# Any time a tool is called, we return to the chatbot to decide the next step\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.set_entry_point(\"chatbot\")" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)\n\ntool_node = ToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n tools_condition,\n)\n# Any time a tool is called, we return to the chatbot to decide the next step\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")"] }, { "cell_type": "markdown", @@ -934,9 +682,7 @@ "id": "a06548bf-81fa-4436-b4c1-f68601fb4187", "metadata": {}, "outputs": [], - "source": [ - "graph = graph_builder.compile(checkpointer=memory)" - ] + "source": ["graph = graph_builder.compile(checkpointer=memory)"] }, { "cell_type": "markdown", @@ -963,15 +709,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -987,9 +725,7 @@ "id": "be7b5abb-04ef-4d53-83d1-d4d3139cc43a", "metadata": {}, "outputs": [], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"1\"}}" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"1\"}}"] }, { "cell_type": "markdown", @@ -1018,16 +754,7 @@ ] } ], - "source": [ - "user_input = \"Hi there! My name is Will.\"\n", - "\n", - "# The config is the **second positional argument** to stream() or invoke()!\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["user_input = \"Hi there! My name is Will.\"\n\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -1058,16 +785,7 @@ ] } ], - "source": [ - "user_input = \"Remember my name?\"\n", - "\n", - "# The config is the **second positional argument** to stream() or invoke()!\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["user_input = \"Remember my name?\"\n\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -1098,16 +816,7 @@ ] } ], - "source": [ - "# The only difference is we change the `thread_id` here to \"2\" instead of \"1\"\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]},\n", - " {\"configurable\": {\"thread_id\": \"2\"}},\n", - " stream_mode=\"values\",\n", - ")\n", - "for event in events:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["# The only difference is we change the `thread_id` here to \"2\" instead of \"1\"\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]},\n {\"configurable\": {\"thread_id\": \"2\"}},\n stream_mode=\"values\",\n)\nfor event in events:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -1136,10 +845,7 @@ "output_type": "execute_result" } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "snapshot" - ] + "source": ["snapshot = graph.get_state(config)\nsnapshot"] }, { "cell_type": "code", @@ -1158,9 +864,7 @@ "output_type": "execute_result" } ], - "source": [ - "snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)" - ] + "source": ["snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)"] }, { "cell_type": "markdown", @@ -1256,51 +960,7 @@ ] } ], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode, tools_condition\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "graph_builder = StateGraph(State)\n", - "\n", - "\n", - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm_with_tools = llm.bind_tools(tools)\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)\n", - "\n", - "tool_node = ToolNode(tools=[tool])\n", - "graph_builder.add_node(\"tools\", tool_node)\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " tools_condition,\n", - ")\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.set_entry_point(\"chatbot\")" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)\n\ntool_node = ToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n tools_condition,\n)\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")"] }, { "cell_type": "markdown", @@ -1316,15 +976,7 @@ "id": "b0883e32-1a39-4ce9-ae32-bbd66708fd84", "metadata": {}, "outputs": [], - "source": [ - "graph = graph_builder.compile(\n", - " checkpointer=memory,\n", - " # This is new!\n", - " interrupt_before=[\"tools\"],\n", - " # Note: can also interrupt __after__ actions, if desired.\n", - " # interrupt_after=[\"tools\"]\n", - ")" - ] + "source": ["graph = graph_builder.compile(\n checkpointer=memory,\n # This is new!\n interrupt_before=[\"tools\"],\n # Note: can also interrupt __after__ actions, if desired.\n # interrupt_after=[\"tools\"]\n)"] }, { "cell_type": "code", @@ -1350,17 +1002,7 @@ ] } ], - "source": [ - "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "# The config is the **second positional argument** to stream() or invoke()!\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -1387,10 +1029,7 @@ "output_type": "execute_result" } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "snapshot.next" - ] + "source": ["snapshot = graph.get_state(config)\nsnapshot.next"] }, { "cell_type": "markdown", @@ -1419,10 +1058,7 @@ "output_type": "execute_result" } ], - "source": [ - "existing_message = snapshot.values[\"messages\"][-1]\n", - "existing_message.tool_calls" - ] + "source": ["existing_message = snapshot.values[\"messages\"][-1]\nexisting_message.tool_calls"] }, { "cell_type": "markdown", @@ -1462,13 +1098,7 @@ ] } ], - "source": [ - "# `None` will append nothing new to the current state, letting it resume as if it had never been interrupted\n", - "events = graph.stream(None, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["# `None` will append nothing new to the current state, letting it resume as if it had never been interrupted\nevents = graph.stream(None, config, stream_mode=\"values\")\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -1572,65 +1202,7 @@ ] } ], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode, tools_condition\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "graph_builder = StateGraph(State)\n", - "\n", - "\n", - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm_with_tools = llm.bind_tools(tools)\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)\n", - "\n", - "tool_node = ToolNode(tools=[tool])\n", - "graph_builder.add_node(\"tools\", tool_node)\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " tools_condition,\n", - ")\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.set_entry_point(\"chatbot\")\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "graph = graph_builder.compile(\n", - " checkpointer=memory,\n", - " # This is new!\n", - " interrupt_before=[\"tools\"],\n", - " # Note: can also interrupt **after** actions, if desired.\n", - " # interrupt_after=[\"tools\"]\n", - ")\n", - "\n", - "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "# The config is the **second positional argument** to stream() or invoke()!\n", - "events = graph.stream({\"messages\": [(\"user\", user_input)]}, config)\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)\n\ntool_node = ToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n tools_condition,\n)\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = graph_builder.compile(\n checkpointer=memory,\n # This is new!\n interrupt_before=[\"tools\"],\n # Note: can also interrupt **after** actions, if desired.\n # interrupt_after=[\"tools\"]\n)\n\nuser_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream({\"messages\": [(\"user\", user_input)]}, config)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -1653,11 +1225,7 @@ ] } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "existing_message = snapshot.values[\"messages\"][-1]\n", - "existing_message.pretty_print()" - ] + "source": ["snapshot = graph.get_state(config)\nexisting_message = snapshot.values[\"messages\"][-1]\nexisting_message.pretty_print()"] }, { "cell_type": "markdown", @@ -1691,31 +1259,7 @@ ] } ], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "answer = (\n", - " \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n", - ")\n", - "new_messages = [\n", - " # The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.\n", - " ToolMessage(content=answer, tool_call_id=existing_message.tool_calls[0][\"id\"]),\n", - " # And then directly \"put words in the LLM's mouth\" by populating its response.\n", - " AIMessage(content=answer),\n", - "]\n", - "\n", - "new_messages[-1].pretty_print()\n", - "graph.update_state(\n", - " # Which state to update\n", - " config,\n", - " # The updated values to provide. The messages in our `State` are \"append-only\", meaning this will be appended\n", - " # to the existing state. We will review how to update existing messages in the next section!\n", - " {\"messages\": new_messages},\n", - ")\n", - "\n", - "print(\"\\n\\nLast 2 messages;\")\n", - "print(graph.get_state(config).values[\"messages\"][-2:])" - ] + "source": ["from langchain_core.messages import AIMessage\n\nanswer = (\n \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n)\nnew_messages = [\n # The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.\n ToolMessage(content=answer, tool_call_id=existing_message.tool_calls[0][\"id\"]),\n # And then directly \"put words in the LLM's mouth\" by populating its response.\n AIMessage(content=answer),\n]\n\nnew_messages[-1].pretty_print()\ngraph.update_state(\n # Which state to update\n config,\n # The updated values to provide. The messages in our `State` are \"append-only\", meaning this will be appended\n # to the existing state. We will review how to update existing messages in the next section!\n {\"messages\": new_messages},\n)\n\nprint(\"\\n\\nLast 2 messages;\")\nprint(graph.get_state(config).values[\"messages\"][-2:])"] }, { "cell_type": "markdown", @@ -1754,15 +1298,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.update_state(\n", - " config,\n", - " {\"messages\": [AIMessage(content=\"I'm an AI expert!\")]},\n", - " # Which node for this function to act as. It will automatically continue\n", - " # processing as if this node just ran.\n", - " as_node=\"chatbot\",\n", - ")" - ] + "source": ["graph.update_state(\n config,\n {\"messages\": [AIMessage(content=\"I'm an AI expert!\")]},\n # Which node for this function to act as. It will automatically continue\n # processing as if this node just ran.\n as_node=\"chatbot\",\n)"] }, { "cell_type": "markdown", @@ -1789,15 +1325,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -1822,11 +1350,7 @@ ] } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "print(snapshot.values[\"messages\"][-3:])\n", - "print(snapshot.next)" - ] + "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][-3:])\nprint(snapshot.next)"] }, { "cell_type": "markdown", @@ -1866,16 +1390,7 @@ ] } ], - "source": [ - "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}} # we'll use thread_id = 2 here\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"2\"}} # we'll use thread_id = 2 here\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -1919,31 +1434,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "snapshot = graph.get_state(config)\n", - "existing_message = snapshot.values[\"messages\"][-1]\n", - "print(\"Original\")\n", - "print(\"Message ID\", existing_message.id)\n", - "print(existing_message.tool_calls[0])\n", - "new_tool_call = existing_message.tool_calls[0].copy()\n", - "new_tool_call[\"args\"][\"query\"] = \"LangGraph human-in-the-loop workflow\"\n", - "new_message = AIMessage(\n", - " content=existing_message.content,\n", - " tool_calls=[new_tool_call],\n", - " # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages\n", - " id=existing_message.id,\n", - ")\n", - "\n", - "print(\"Updated\")\n", - "print(new_message.tool_calls[0])\n", - "print(\"Message ID\", new_message.id)\n", - "graph.update_state(config, {\"messages\": [new_message]})\n", - "\n", - "print(\"\\n\\nTool calls\")\n", - "graph.get_state(config).values[\"messages\"][-1].tool_calls" - ] + "source": ["from langchain_core.messages import AIMessage\n\nsnapshot = graph.get_state(config)\nexisting_message = snapshot.values[\"messages\"][-1]\nprint(\"Original\")\nprint(\"Message ID\", existing_message.id)\nprint(existing_message.tool_calls[0])\nnew_tool_call = existing_message.tool_calls[0].copy()\nnew_tool_call[\"args\"][\"query\"] = \"LangGraph human-in-the-loop workflow\"\nnew_message = AIMessage(\n content=existing_message.content,\n tool_calls=[new_tool_call],\n # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages\n id=existing_message.id,\n)\n\nprint(\"Updated\")\nprint(new_message.tool_calls[0])\nprint(\"Message ID\", new_message.id)\ngraph.update_state(config, {\"messages\": [new_message]})\n\nprint(\"\\n\\nTool calls\")\ngraph.get_state(config).values[\"messages\"][-1].tool_calls"] }, { "cell_type": "markdown", @@ -1983,12 +1474,7 @@ ] } ], - "source": [ - "events = graph.stream(None, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["events = graph.stream(None, config, stream_mode=\"values\")\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -2023,21 +1509,7 @@ ] } ], - "source": [ - "events = graph.stream(\n", - " {\n", - " \"messages\": (\n", - " \"user\",\n", - " \"Remember what I'm learning about?\",\n", - " )\n", - " },\n", - " config,\n", - " stream_mode=\"values\",\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["events = graph.stream(\n {\n \"messages\": (\n \"user\",\n \"Remember what I'm learning about?\",\n )\n },\n config,\n stream_mode=\"values\",\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -2071,25 +1543,7 @@ "id": "3cf7e042-1718-4625-ae30-a9917f595449", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import BaseMessage\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode, tools_condition\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - " # This flag is new\n", - " ask_human: bool" - ] + "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n # This flag is new\n ask_human: bool"] }, { "cell_type": "markdown", @@ -2105,18 +1559,7 @@ "id": "e5192e54-6a28-42fe-a8a7-62d45d61f994", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel\n", - "\n", - "\n", - "class RequestAssistance(BaseModel):\n", - " \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n", - "\n", - " To use this function, relay the user's 'request' so the expert can provide the right guidance.\n", - " \"\"\"\n", - "\n", - " request: str" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel\n\n\nclass RequestAssistance(BaseModel):\n \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n\n To use this function, relay the user's 'request' so the expert can provide the right guidance.\n \"\"\"\n\n request: str"] }, { "cell_type": "markdown", @@ -2141,24 +1584,7 @@ ] } ], - "source": [ - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "# We can bind the llm to a tool definition, a pydantic model, or a json schema\n", - "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " response = llm_with_tools.invoke(state[\"messages\"])\n", - " ask_human = False\n", - " if (\n", - " response.tool_calls\n", - " and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n", - " ):\n", - " ask_human = True\n", - " return {\"messages\": [response], \"ask_human\": ask_human}" - ] + "source": ["tool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n# We can bind the llm to a tool definition, a pydantic model, or a json schema\nllm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n\n\ndef chatbot(state: State):\n response = llm_with_tools.invoke(state[\"messages\"])\n ask_human = False\n if (\n response.tool_calls\n and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n ):\n ask_human = True\n return {\"messages\": [response], \"ask_human\": ask_human}"] }, { "cell_type": "markdown", @@ -2174,12 +1600,7 @@ "id": "3f4464d2-288b-4689-aaf0-329a55dcb85c", "metadata": {}, "outputs": [], - "source": [ - "graph_builder = StateGraph(State)\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)\n", - "graph_builder.add_node(\"tools\", ToolNode(tools=[tool]))" - ] + "source": ["graph_builder = StateGraph(State)\n\ngraph_builder.add_node(\"chatbot\", chatbot)\ngraph_builder.add_node(\"tools\", ToolNode(tools=[tool]))"] }, { "cell_type": "markdown", @@ -2195,36 +1616,7 @@ "id": "1d70b5a4-ce50-47dc-aa43-ffb5c48c46fc", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage, ToolMessage\n", - "\n", - "\n", - "def create_response(response: str, ai_message: AIMessage):\n", - " return ToolMessage(\n", - " content=response,\n", - " tool_call_id=ai_message.tool_calls[0][\"id\"],\n", - " )\n", - "\n", - "\n", - "def human_node(state: State):\n", - " new_messages = []\n", - " if not isinstance(state[\"messages\"][-1], ToolMessage):\n", - " # Typically, the user will have updated the state during the interrupt.\n", - " # If they choose not to, we will include a placeholder ToolMessage to\n", - " # let the LLM continue.\n", - " new_messages.append(\n", - " create_response(\"No response from human.\", state[\"messages\"][-1])\n", - " )\n", - " return {\n", - " # Append the new messages\n", - " \"messages\": new_messages,\n", - " # Unset the flag\n", - " \"ask_human\": False,\n", - " }\n", - "\n", - "\n", - "graph_builder.add_node(\"human\", human_node)" - ] + "source": ["from langchain_core.messages import AIMessage, ToolMessage\n\n\ndef create_response(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response,\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef human_node(state: State):\n new_messages = []\n if not isinstance(state[\"messages\"][-1], ToolMessage):\n # Typically, the user will have updated the state during the interrupt.\n # If they choose not to, we will include a placeholder ToolMessage to\n # let the LLM continue.\n new_messages.append(\n create_response(\"No response from human.\", state[\"messages\"][-1])\n )\n return {\n # Append the new messages\n \"messages\": new_messages,\n # Unset the flag\n \"ask_human\": False,\n }\n\n\ngraph_builder.add_node(\"human\", human_node)"] }, { "cell_type": "markdown", @@ -2242,20 +1634,7 @@ "id": "586a0d07-8303-47f4-b3cf-3bdd043e762b", "metadata": {}, "outputs": [], - "source": [ - "def select_next_node(state: State):\n", - " if state[\"ask_human\"]:\n", - " return \"human\"\n", - " # Otherwise, we can route as before\n", - " return tools_condition(state)\n", - "\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " select_next_node,\n", - " {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n", - ")" - ] + "source": ["def select_next_node(state: State):\n if state[\"ask_human\"]:\n return \"human\"\n # Otherwise, we can route as before\n return tools_condition(state)\n\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n select_next_node,\n {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n)"] }, { "cell_type": "markdown", @@ -2271,18 +1650,7 @@ "id": "84101737-0048-4635-9f68-45b0c508b6b6", "metadata": {}, "outputs": [], - "source": [ - "# The rest is the same\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.add_edge(\"human\", \"chatbot\")\n", - "graph_builder.set_entry_point(\"chatbot\")\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "graph = graph_builder.compile(\n", - " checkpointer=memory,\n", - " # We interrupt before 'human' here instead.\n", - " interrupt_before=[\"human\"],\n", - ")" - ] + "source": ["# The rest is the same\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(\"human\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = graph_builder.compile(\n checkpointer=memory,\n # We interrupt before 'human' here instead.\n interrupt_before=[\"human\"],\n)"] }, { "cell_type": "markdown", @@ -2309,15 +1677,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -2353,17 +1713,7 @@ ] } ], - "source": [ - "user_input = \"I need some expert guidance for building this AI agent. Could you request assistance for me?\"\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "# The config is the **second positional argument** to stream() or invoke()!\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["user_input = \"I need some expert guidance for building this AI agent. Could you request assistance for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -2390,10 +1740,7 @@ "output_type": "execute_result" } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "snapshot.next" - ] + "source": ["snapshot = graph.get_state(config)\nsnapshot.next"] }, { "cell_type": "markdown", @@ -2425,15 +1772,7 @@ "output_type": "execute_result" } ], - "source": [ - "ai_message = snapshot.values[\"messages\"][-1]\n", - "human_response = (\n", - " \"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.\"\n", - " \" It's much more reliable and extensible than simple autonomous agents.\"\n", - ")\n", - "tool_message = create_response(human_response, ai_message)\n", - "graph.update_state(config, {\"messages\": [tool_message]})" - ] + "source": ["ai_message = snapshot.values[\"messages\"][-1]\nhuman_response = (\n \"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.\"\n \" It's much more reliable and extensible than simple autonomous agents.\"\n)\ntool_message = create_response(human_response, ai_message)\ngraph.update_state(config, {\"messages\": [tool_message]})"] }, { "cell_type": "markdown", @@ -2462,9 +1801,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.get_state(config).values[\"messages\"]" - ] + "source": ["graph.get_state(config).values[\"messages\"]"] }, { "cell_type": "markdown", @@ -2493,12 +1830,7 @@ ] } ], - "source": [ - "events = graph.stream(None, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["events = graph.stream(None, config, stream_mode=\"values\")\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -2647,108 +1979,7 @@ "id": "bb8a02de-a21b-4ef6-a714-7d6e44435e3a", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated, Literal\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.messages import AIMessage, BaseMessage, ToolMessage\n", - "from langchain_core.pydantic_v1 import BaseModel\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode, tools_condition\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - " # This flag is new\n", - " ask_human: bool\n", - "\n", - "\n", - "class RequestAssistance(BaseModel):\n", - " \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n", - "\n", - " To use this function, relay the user's 'request' so the expert can provide the right guidance.\n", - " \"\"\"\n", - "\n", - " request: str\n", - "\n", - "\n", - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "# We can bind the llm to a tool definition, a pydantic model, or a json schema\n", - "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n", - "\n", - "\n", - "def chatbot(state: State):\n", - " response = llm_with_tools.invoke(state[\"messages\"])\n", - " ask_human = False\n", - " if (\n", - " response.tool_calls\n", - " and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n", - " ):\n", - " ask_human = True\n", - " return {\"messages\": [response], \"ask_human\": ask_human}\n", - "\n", - "\n", - "graph_builder = StateGraph(State)\n", - "\n", - "graph_builder.add_node(\"chatbot\", chatbot)\n", - "graph_builder.add_node(\"tools\", ToolNode(tools=[tool]))\n", - "\n", - "\n", - "def create_response(response: str, ai_message: AIMessage):\n", - " return ToolMessage(\n", - " content=response,\n", - " tool_call_id=ai_message.tool_calls[0][\"id\"],\n", - " )\n", - "\n", - "\n", - "def human_node(state: State):\n", - " new_messages = []\n", - " if not isinstance(state[\"messages\"][-1], ToolMessage):\n", - " # Typically, the user will have updated the state during the interrupt.\n", - " # If they choose not to, we will include a placeholder ToolMessage to\n", - " # let the LLM continue.\n", - " new_messages.append(\n", - " create_response(\"No response from human.\", state[\"messages\"][-1])\n", - " )\n", - " return {\n", - " # Append the new messages\n", - " \"messages\": new_messages,\n", - " # Unset the flag\n", - " \"ask_human\": False,\n", - " }\n", - "\n", - "\n", - "graph_builder.add_node(\"human\", human_node)\n", - "\n", - "\n", - "def select_next_node(state: State) -> Literal[\"human\", \"tools\", \"__end__\"]:\n", - " if state[\"ask_human\"]:\n", - " return \"human\"\n", - " # Otherwise, we can route as before\n", - " return tools_condition(state)\n", - "\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " select_next_node,\n", - " {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n", - ")\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.add_edge(\"human\", \"chatbot\")\n", - "graph_builder.set_entry_point(\"chatbot\")\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "graph = graph_builder.compile(\n", - " checkpointer=memory,\n", - " interrupt_before=[\"human\"],\n", - ")" - ] + "source": ["from typing import Annotated, Literal\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import AIMessage, BaseMessage, ToolMessage\nfrom langchain_core.pydantic_v1 import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n # This flag is new\n ask_human: bool\n\n\nclass RequestAssistance(BaseModel):\n \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n\n To use this function, relay the user's 'request' so the expert can provide the right guidance.\n \"\"\"\n\n request: str\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n# We can bind the llm to a tool definition, a pydantic model, or a json schema\nllm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n\n\ndef chatbot(state: State):\n response = llm_with_tools.invoke(state[\"messages\"])\n ask_human = False\n if (\n response.tool_calls\n and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n ):\n ask_human = True\n return {\"messages\": [response], \"ask_human\": ask_human}\n\n\ngraph_builder = StateGraph(State)\n\ngraph_builder.add_node(\"chatbot\", chatbot)\ngraph_builder.add_node(\"tools\", ToolNode(tools=[tool]))\n\n\ndef create_response(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response,\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef human_node(state: State):\n new_messages = []\n if not isinstance(state[\"messages\"][-1], ToolMessage):\n # Typically, the user will have updated the state during the interrupt.\n # If they choose not to, we will include a placeholder ToolMessage to\n # let the LLM continue.\n new_messages.append(\n create_response(\"No response from human.\", state[\"messages\"][-1])\n )\n return {\n # Append the new messages\n \"messages\": new_messages,\n # Unset the flag\n \"ask_human\": False,\n }\n\n\ngraph_builder.add_node(\"human\", human_node)\n\n\ndef select_next_node(state: State) -> Literal[\"human\", \"tools\", \"__end__\"]:\n if state[\"ask_human\"]:\n return \"human\"\n # Otherwise, we can route as before\n return tools_condition(state)\n\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n select_next_node,\n {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n)\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(\"human\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = graph_builder.compile(\n checkpointer=memory,\n interrupt_before=[\"human\"],\n)"] }, { "cell_type": "code", @@ -2767,15 +1998,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -2828,21 +2051,7 @@ ] } ], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "events = graph.stream(\n", - " {\n", - " \"messages\": [\n", - " (\"user\", \"I'm learning LangGraph. Could you do some research on it for me?\")\n", - " ]\n", - " },\n", - " config,\n", - " stream_mode=\"values\",\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"1\"}}\nevents = graph.stream(\n {\n \"messages\": [\n (\"user\", \"I'm learning LangGraph. Could you do some research on it for me?\")\n ]\n },\n config,\n stream_mode=\"values\",\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -2887,20 +2096,7 @@ ] } ], - "source": [ - "events = graph.stream(\n", - " {\n", - " \"messages\": [\n", - " (\"user\", \"Ya that's helpful. Maybe I'll build an autonomous agent with it!\")\n", - " ]\n", - " },\n", - " config,\n", - " stream_mode=\"values\",\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["events = graph.stream(\n {\n \"messages\": [\n (\"user\", \"Ya that's helpful. Maybe I'll build an autonomous agent with it!\")\n ]\n },\n config,\n stream_mode=\"values\",\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -2939,15 +2135,7 @@ ] } ], - "source": [ - "to_replay = None\n", - "for state in graph.get_state_history(config):\n", - " print(\"Num Messages: \", len(state.values[\"messages\"]), \"Next: \", state.next)\n", - " print(\"-\" * 80)\n", - " if len(state.values[\"messages\"]) == 6:\n", - " # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.\n", - " to_replay = state" - ] + "source": ["to_replay = None\nfor state in graph.get_state_history(config):\n print(\"Num Messages: \", len(state.values[\"messages\"]), \"Next: \", state.next)\n print(\"-\" * 80)\n if len(state.values[\"messages\"]) == 6:\n # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.\n to_replay = state"] }, { "cell_type": "markdown", @@ -2974,10 +2162,7 @@ ] } ], - "source": [ - "print(to_replay.next)\n", - "print(to_replay.config)" - ] + "source": ["print(to_replay.next)\nprint(to_replay.config)"] }, { "cell_type": "markdown", @@ -3023,12 +2208,7 @@ ] } ], - "source": [ - "# The `thread_ts` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.\n", - "for event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["# The `thread_ts` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.\nfor event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", diff --git a/examples/lats/lats.ipynb b/examples/lats/lats.ipynb index 3e1a5cca2..a0a9440b5 100644 --- a/examples/lats/lats.ipynb +++ b/examples/lats/lats.ipynb @@ -37,11 +37,7 @@ "id": "dcc9159b-cc8c-426d-9670-3e8ada06723f", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U --quiet langchain langgraph langchain_openai\n", - "%pip install -U --quiet tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install -U --quiet langchain langgraph langchain_openai\n%pip install -U --quiet tavily-python"] }, { "cell_type": "code", @@ -49,27 +45,7 @@ "id": "a177ecc9-0c96-460f-9b39-9c1ce54754f1", "metadata": {}, "outputs": [], - "source": [ - "from __future__ import annotations # noqa: F404\n", - "\n", - "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\"] = \"LATS\"\n", - "\n", - "_set_if_undefined(\"OPENAI_API_KEY\")\n", - "_set_if_undefined(\"TAVILY_API_KEY\")" - ] + "source": ["from __future__ import annotations # noqa: F404\n\nimport 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\"] = \"LATS\"\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"] }, { "cell_type": "markdown", @@ -93,134 +69,7 @@ "id": "54c6f319-3966-4f66-aa7b-50e249189111", "metadata": {}, "outputs": [], - "source": [ - "import math\n", - "from collections import deque\n", - "from typing import Optional\n", - "\n", - "from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage\n", - "\n", - "\n", - "class Node:\n", - " def __init__(\n", - " self,\n", - " messages: list[BaseMessage],\n", - " reflection: Reflection,\n", - " parent: Optional[Node] = None,\n", - " ):\n", - " self.messages = messages\n", - " self.parent = parent\n", - " self.children = []\n", - " self.value = 0\n", - " self.visits = 0\n", - " self.reflection = reflection\n", - " self.depth = parent.depth + 1 if parent is not None else 1\n", - " self._is_solved = reflection.found_solution if reflection else False\n", - " if self._is_solved:\n", - " self._mark_tree_as_solved()\n", - " self.backpropagate(reflection.normalized_score)\n", - "\n", - " def __repr__(self) -> str:\n", - " return (\n", - " f\"\"\n", - " )\n", - "\n", - " @property\n", - " def is_solved(self):\n", - " \"\"\"If any solutions exist, we can end the search.\"\"\"\n", - " return self._is_solved\n", - "\n", - " @property\n", - " def is_terminal(self):\n", - " return not self.children\n", - "\n", - " @property\n", - " def best_child(self):\n", - " \"\"\"Select the child with the highest UCT to search next.\"\"\"\n", - " if not self.children:\n", - " return None\n", - " all_nodes = self._get_all_children()\n", - " return max(all_nodes, key=lambda child: child.upper_confidence_bound())\n", - "\n", - " @property\n", - " def best_child_score(self):\n", - " \"\"\"Return the child with the highest value.\"\"\"\n", - " if not self.children:\n", - " return None\n", - " return max(self.children, key=lambda child: int(child.is_solved) * child.value)\n", - "\n", - " @property\n", - " def height(self) -> int:\n", - " \"\"\"Check for how far we've rolled out the tree.\"\"\"\n", - " if self.children:\n", - " return 1 + max([child.height for child in self.children])\n", - " return 1\n", - "\n", - " def upper_confidence_bound(self, exploration_weight=1.0):\n", - " \"\"\"Return the UCT score. This helps balance exploration vs. exploitation of a branch.\"\"\"\n", - " if self.parent is None:\n", - " raise ValueError(\"Cannot obtain UCT from root node\")\n", - " if self.visits == 0:\n", - " return self.value\n", - " # Encourages exploitation of high-value trajectories\n", - " average_reward = self.value / self.visits\n", - " # Encourages exploration of less-visited trajectories\n", - " exploration_term = math.sqrt(math.log(self.parent.visits) / self.visits)\n", - " return average_reward + exploration_weight * exploration_term\n", - "\n", - " def backpropagate(self, reward: float):\n", - " \"\"\"Update the score of this node and its parents.\"\"\"\n", - " node = self\n", - " while node:\n", - " node.visits += 1\n", - " node.value = (node.value * (node.visits - 1) + reward) / node.visits\n", - " node = node.parent\n", - "\n", - " def get_messages(self, include_reflections: bool = True):\n", - " if include_reflections:\n", - " return self.messages + [self.reflection.as_message()]\n", - " return self.messages\n", - "\n", - " def get_trajectory(self, include_reflections: bool = True) -> list[BaseMessage]:\n", - " \"\"\"Get messages representing this search branch.\"\"\"\n", - " messages = []\n", - " node = self\n", - " while node:\n", - " messages.extend(\n", - " node.get_messages(include_reflections=include_reflections)[::-1]\n", - " )\n", - " node = node.parent\n", - " # Reverse the final back-tracked trajectory to return in the correct order\n", - " return messages[::-1] # root solution, reflection, child 1, ...\n", - "\n", - " def _get_all_children(self):\n", - " all_nodes = []\n", - " nodes = deque()\n", - " nodes.append(self)\n", - " while nodes:\n", - " node = nodes.popleft()\n", - " all_nodes.extend(node.children)\n", - " for n in node.children:\n", - " nodes.append(n)\n", - " return all_nodes\n", - "\n", - " def get_best_solution(self):\n", - " \"\"\"Return the best solution from within the current sub-tree.\"\"\"\n", - " all_nodes = [self] + self._get_all_children()\n", - " best_node = max(\n", - " all_nodes,\n", - " # We filter out all non-terminal, non-solution trajectories\n", - " key=lambda node: int(node.is_terminal and node.is_solved) * node.value,\n", - " )\n", - " return best_node\n", - "\n", - " def _mark_tree_as_solved(self):\n", - " parent = self.parent\n", - " while parent:\n", - " parent._is_solved = True\n", - " parent = parent.parent" - ] + "source": ["import math\nfrom collections import deque\nfrom typing import Optional\n\nfrom langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage\n\n\nclass Node:\n def __init__(\n self,\n messages: list[BaseMessage],\n reflection: Reflection,\n parent: Optional[Node] = None,\n ):\n self.messages = messages\n self.parent = parent\n self.children = []\n self.value = 0\n self.visits = 0\n self.reflection = reflection\n self.depth = parent.depth + 1 if parent is not None else 1\n self._is_solved = reflection.found_solution if reflection else False\n if self._is_solved:\n self._mark_tree_as_solved()\n self.backpropagate(reflection.normalized_score)\n\n def __repr__(self) -> str:\n return (\n f\"\"\n )\n\n @property\n def is_solved(self):\n \"\"\"If any solutions exist, we can end the search.\"\"\"\n return self._is_solved\n\n @property\n def is_terminal(self):\n return not self.children\n\n @property\n def best_child(self):\n \"\"\"Select the child with the highest UCT to search next.\"\"\"\n if not self.children:\n return None\n all_nodes = self._get_all_children()\n return max(all_nodes, key=lambda child: child.upper_confidence_bound())\n\n @property\n def best_child_score(self):\n \"\"\"Return the child with the highest value.\"\"\"\n if not self.children:\n return None\n return max(self.children, key=lambda child: int(child.is_solved) * child.value)\n\n @property\n def height(self) -> int:\n \"\"\"Check for how far we've rolled out the tree.\"\"\"\n if self.children:\n return 1 + max([child.height for child in self.children])\n return 1\n\n def upper_confidence_bound(self, exploration_weight=1.0):\n \"\"\"Return the UCT score. This helps balance exploration vs. exploitation of a branch.\"\"\"\n if self.parent is None:\n raise ValueError(\"Cannot obtain UCT from root node\")\n if self.visits == 0:\n return self.value\n # Encourages exploitation of high-value trajectories\n average_reward = self.value / self.visits\n # Encourages exploration of less-visited trajectories\n exploration_term = math.sqrt(math.log(self.parent.visits) / self.visits)\n return average_reward + exploration_weight * exploration_term\n\n def backpropagate(self, reward: float):\n \"\"\"Update the score of this node and its parents.\"\"\"\n node = self\n while node:\n node.visits += 1\n node.value = (node.value * (node.visits - 1) + reward) / node.visits\n node = node.parent\n\n def get_messages(self, include_reflections: bool = True):\n if include_reflections:\n return self.messages + [self.reflection.as_message()]\n return self.messages\n\n def get_trajectory(self, include_reflections: bool = True) -> list[BaseMessage]:\n \"\"\"Get messages representing this search branch.\"\"\"\n messages = []\n node = self\n while node:\n messages.extend(\n node.get_messages(include_reflections=include_reflections)[::-1]\n )\n node = node.parent\n # Reverse the final back-tracked trajectory to return in the correct order\n return messages[::-1] # root solution, reflection, child 1, ...\n\n def _get_all_children(self):\n all_nodes = []\n nodes = deque()\n nodes.append(self)\n while nodes:\n node = nodes.popleft()\n all_nodes.extend(node.children)\n for n in node.children:\n nodes.append(n)\n return all_nodes\n\n def get_best_solution(self):\n \"\"\"Return the best solution from within the current sub-tree.\"\"\"\n all_nodes = [self] + self._get_all_children()\n best_node = max(\n all_nodes,\n # We filter out all non-terminal, non-solution trajectories\n key=lambda node: int(node.is_terminal and node.is_solved) * node.value,\n )\n return best_node\n\n def _mark_tree_as_solved(self):\n parent = self.parent\n while parent:\n parent._is_solved = True\n parent = parent.parent"] }, { "cell_type": "markdown", @@ -238,16 +87,7 @@ "id": "e10c94ba-9daa-4899-97ce-4f28428c2c38", "metadata": {}, "outputs": [], - "source": [ - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class TreeState(TypedDict):\n", - " # The full tree\n", - " root: Node\n", - " # The original input\n", - " input: str" - ] + "source": ["from typing_extensions import TypedDict\n\n\nclass TreeState(TypedDict):\n # The full tree\n root: Node\n # The original input\n input: str"] }, { "cell_type": "markdown", @@ -270,11 +110,7 @@ "id": "48738896-42ac-47eb-b482-0d4d4dd86c87", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "llm = ChatOpenAI(model=\"gpt-4o\")" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nllm = ChatOpenAI(model=\"gpt-4o\")"] }, { "cell_type": "markdown", @@ -292,17 +128,7 @@ "id": "55c2aff3-f454-43da-8f45-1a3d46523cd5", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n", - "\n", - "from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation\n", - "\n", - "search = TavilySearchAPIWrapper()\n", - "tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)\n", - "tools = [tavily_tool]\n", - "tool_executor = ToolExecutor(tools=tools)" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)\ntools = [tavily_tool]\ntool_executor = ToolExecutor(tools=tools)"] }, { "cell_type": "markdown", @@ -321,68 +147,7 @@ "id": "ddfd1750-c265-4b29-b505-83b1c5e2d30e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.output_parsers.openai_tools import (\n", - " JsonOutputToolsParser,\n", - " PydanticToolsParser,\n", - ")\n", - "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_core.runnables import chain as as_runnable\n", - "\n", - "\n", - "class Reflection(BaseModel):\n", - " reflections: str = Field(\n", - " description=\"The critique and reflections on the sufficiency, superfluency,\"\n", - " \" and general quality of the response\"\n", - " )\n", - " score: int = Field(\n", - " description=\"Score from 0-10 on the quality of the candidate response.\",\n", - " gte=0,\n", - " lte=10,\n", - " )\n", - " found_solution: bool = Field(\n", - " description=\"Whether the response has fully solved the question or task.\"\n", - " )\n", - "\n", - " def as_message(self):\n", - " return HumanMessage(\n", - " content=f\"Reasoning: {self.reflections}\\nScore: {self.score}\"\n", - " )\n", - "\n", - " @property\n", - " def normalized_score(self) -> float:\n", - " return self.score / 10.0\n", - "\n", - "\n", - "prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"Reflect and grade the assistant response to the user question below.\",\n", - " ),\n", - " (\"user\", \"{input}\"),\n", - " MessagesPlaceholder(variable_name=\"candidate\"),\n", - " ]\n", - ")\n", - "\n", - "reflection_llm_chain = (\n", - " prompt\n", - " | llm.bind_tools(tools=[Reflection], tool_choice=\"Reflection\").with_config(\n", - " run_name=\"Reflection\"\n", - " )\n", - " | PydanticToolsParser(tools=[Reflection])\n", - ")\n", - "\n", - "\n", - "@as_runnable\n", - "def reflection_chain(inputs) -> Reflection:\n", - " tool_choices = reflection_llm_chain.invoke(inputs)\n", - " reflection = tool_choices[0]\n", - " if not isinstance(inputs[\"candidate\"][-1], AIMessage):\n", - " reflection.found_solution = False\n", - " return reflection" - ] + "source": ["from langchain_core.output_parsers.openai_tools import (\n JsonOutputToolsParser,\n PydanticToolsParser,\n)\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_core.runnables import chain as as_runnable\n\n\nclass Reflection(BaseModel):\n reflections: str = Field(\n description=\"The critique and reflections on the sufficiency, superfluency,\"\n \" and general quality of the response\"\n )\n score: int = Field(\n description=\"Score from 0-10 on the quality of the candidate response.\",\n gte=0,\n lte=10,\n )\n found_solution: bool = Field(\n description=\"Whether the response has fully solved the question or task.\"\n )\n\n def as_message(self):\n return HumanMessage(\n content=f\"Reasoning: {self.reflections}\\nScore: {self.score}\"\n )\n\n @property\n def normalized_score(self) -> float:\n return self.score / 10.0\n\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"Reflect and grade the assistant response to the user question below.\",\n ),\n (\"user\", \"{input}\"),\n MessagesPlaceholder(variable_name=\"candidate\"),\n ]\n)\n\nreflection_llm_chain = (\n prompt\n | llm.bind_tools(tools=[Reflection], tool_choice=\"Reflection\").with_config(\n run_name=\"Reflection\"\n )\n | PydanticToolsParser(tools=[Reflection])\n)\n\n\n@as_runnable\ndef reflection_chain(inputs) -> Reflection:\n tool_choices = reflection_llm_chain.invoke(inputs)\n reflection = tool_choices[0]\n if not isinstance(inputs[\"candidate\"][-1], AIMessage):\n reflection.found_solution = False\n return reflection"] }, { "cell_type": "markdown", @@ -400,29 +165,7 @@ "id": "72fc5363-f0f3-4362-8499-14eb583bd75b", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.prompt_values import ChatPromptValue\n", - "from langchain_core.runnables import RunnableConfig\n", - "\n", - "prompt_template = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are an AI assistant.\",\n", - " ),\n", - " (\"user\", \"{input}\"),\n", - " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", - " ]\n", - ")\n", - "\n", - "\n", - "initial_answer_chain = prompt_template | llm.bind_tools(tools=tools).with_config(\n", - " run_name=\"GenerateInitialCandidate\"\n", - ")\n", - "\n", - "\n", - "parser = JsonOutputToolsParser(return_id=True)" - ] + "source": ["from langchain_core.prompt_values import ChatPromptValue\nfrom langchain_core.runnables import RunnableConfig\n\nprompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an AI assistant.\",\n ),\n (\"user\", \"{input}\"),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ninitial_answer_chain = prompt_template | llm.bind_tools(tools=tools).with_config(\n run_name=\"GenerateInitialCandidate\"\n)\n\n\nparser = JsonOutputToolsParser(return_id=True)"] }, { "cell_type": "code", @@ -441,12 +184,7 @@ "output_type": "execute_result" } ], - "source": [ - "initial_response = initial_answer_chain.invoke(\n", - " {\"input\": \"Write a research report on lithium pollution.\"}\n", - ")\n", - "initial_response" - ] + "source": ["initial_response = initial_answer_chain.invoke(\n {\"input\": \"Write a research report on lithium pollution.\"}\n)\ninitial_response"] }, { "cell_type": "markdown", @@ -464,31 +202,7 @@ "id": "5b6b173c-78f5-4ae1-80b3-28c80e68f5c5", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "\n", - "\n", - "# Define the node we will add to the graph\n", - "def generate_initial_response(state: TreeState) -> dict:\n", - " \"\"\"Generate the initial candidate response.\"\"\"\n", - " res = initial_answer_chain.invoke({\"input\": state[\"input\"]})\n", - " parsed = parser.invoke(res)\n", - " tool_responses = tool_executor.batch(\n", - " [ToolInvocation(tool=r[\"type\"], tool_input=r[\"args\"]) for r in parsed]\n", - " )\n", - " output_messages = [res] + [\n", - " ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n", - " for resp, tool_call in zip(tool_responses, parsed)\n", - " ]\n", - " reflection = reflection_chain.invoke(\n", - " {\"input\": state[\"input\"], \"candidate\": output_messages}\n", - " )\n", - " root = Node(output_messages, reflection=reflection)\n", - " return {\n", - " **state,\n", - " \"root\": root,\n", - " }" - ] + "source": ["import json\n\n\n# Define the node we will add to the graph\ndef generate_initial_response(state: TreeState) -> dict:\n \"\"\"Generate the initial candidate response.\"\"\"\n res = initial_answer_chain.invoke({\"input\": state[\"input\"]})\n parsed = parser.invoke(res)\n tool_responses = tool_executor.batch(\n [ToolInvocation(tool=r[\"type\"], tool_input=r[\"args\"]) for r in parsed]\n )\n output_messages = [res] + [\n ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n for resp, tool_call in zip(tool_responses, parsed)\n ]\n reflection = reflection_chain.invoke(\n {\"input\": state[\"input\"], \"candidate\": output_messages}\n )\n root = Node(output_messages, reflection=reflection)\n return {\n **state,\n \"root\": root,\n }"] }, { "cell_type": "markdown", @@ -506,26 +220,7 @@ "id": "550bff9a-86aa-43ad-ad98-506e97c122d2", "metadata": {}, "outputs": [], - "source": [ - "# This generates N candidate values\n", - "# for a single input to sample actions from the environment\n", - "\n", - "\n", - "def generate_candidates(messages: ChatPromptValue, config: RunnableConfig):\n", - " n = config[\"configurable\"].get(\"N\", 5)\n", - " bound_kwargs = llm.bind_tools(tools=tools).kwargs\n", - " chat_result = llm.generate(\n", - " [messages.to_messages()],\n", - " n=n,\n", - " callbacks=config[\"callbacks\"],\n", - " run_name=\"GenerateCandidates\",\n", - " **bound_kwargs,\n", - " )\n", - " return [gen.message for gen in chat_result.generations[0]]\n", - "\n", - "\n", - "expansion_chain = prompt_template | generate_candidates" - ] + "source": ["# This generates N candidate values\n# for a single input to sample actions from the environment\n\n\ndef generate_candidates(messages: ChatPromptValue, config: RunnableConfig):\n n = config[\"configurable\"].get(\"N\", 5)\n bound_kwargs = llm.bind_tools(tools=tools).kwargs\n chat_result = llm.generate(\n [messages.to_messages()],\n n=n,\n callbacks=config[\"callbacks\"],\n run_name=\"GenerateCandidates\",\n **bound_kwargs,\n )\n return [gen.message for gen in chat_result.generations[0]]\n\n\nexpansion_chain = prompt_template | generate_candidates"] }, { "cell_type": "code", @@ -548,10 +243,7 @@ "output_type": "execute_result" } ], - "source": [ - "res = expansion_chain.invoke({\"input\": \"Write a research report on lithium pollution.\"})\n", - "res" - ] + "source": ["res = expansion_chain.invoke({\"input\": \"Write a research report on lithium pollution.\"})\nres"] }, { "cell_type": "markdown", @@ -570,55 +262,7 @@ "id": "d32af859-53e8-46be-8182-7d522be31f54", "metadata": {}, "outputs": [], - "source": [ - "from collections import defaultdict\n", - "\n", - "\n", - "def expand(state: TreeState, config: RunnableConfig) -> dict:\n", - " \"\"\"Starting from the \"best\" node in the tree, generate N candidates for the next step.\"\"\"\n", - " root = state[\"root\"]\n", - " best_candidate: Node = root.best_child if root.children else root\n", - " messages = best_candidate.get_trajectory()\n", - " # Generate N candidates from the single child candidate\n", - " new_candidates = expansion_chain.invoke(\n", - " {\"input\": state[\"input\"], \"messages\": messages}, config\n", - " )\n", - " parsed = parser.batch(new_candidates)\n", - " flattened = [\n", - " (i, tool_call)\n", - " for i, tool_calls in enumerate(parsed)\n", - " for tool_call in tool_calls\n", - " ]\n", - " tool_responses = tool_executor.batch(\n", - " [\n", - " ToolInvocation(tool=tool_call[\"type\"], tool_input=tool_call[\"args\"])\n", - " for _, tool_call in flattened\n", - " ]\n", - " )\n", - " collected_responses = defaultdict(list)\n", - " for (i, tool_call), resp in zip(flattened, tool_responses):\n", - " collected_responses[i].append(\n", - " ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n", - " )\n", - " output_messages = []\n", - " for i, candidate in enumerate(new_candidates):\n", - " output_messages.append([candidate] + collected_responses[i])\n", - "\n", - " # Reflect on each candidate\n", - " # For tasks with external validation, you'd add that here.\n", - " reflections = reflection_chain.batch(\n", - " [{\"input\": state[\"input\"], \"candidate\": msges} for msges in output_messages],\n", - " config,\n", - " )\n", - " # Grow tree\n", - " child_nodes = [\n", - " Node(cand, parent=best_candidate, reflection=reflection)\n", - " for cand, reflection in zip(output_messages, reflections)\n", - " ]\n", - " best_candidate.children.extend(child_nodes)\n", - " # We have already extended the tree directly, so we just return the state\n", - " return state" - ] + "source": ["from collections import defaultdict\n\n\ndef expand(state: TreeState, config: RunnableConfig) -> dict:\n \"\"\"Starting from the \"best\" node in the tree, generate N candidates for the next step.\"\"\"\n root = state[\"root\"]\n best_candidate: Node = root.best_child if root.children else root\n messages = best_candidate.get_trajectory()\n # Generate N candidates from the single child candidate\n new_candidates = expansion_chain.invoke(\n {\"input\": state[\"input\"], \"messages\": messages}, config\n )\n parsed = parser.batch(new_candidates)\n flattened = [\n (i, tool_call)\n for i, tool_calls in enumerate(parsed)\n for tool_call in tool_calls\n ]\n tool_responses = tool_executor.batch(\n [\n ToolInvocation(tool=tool_call[\"type\"], tool_input=tool_call[\"args\"])\n for _, tool_call in flattened\n ]\n )\n collected_responses = defaultdict(list)\n for (i, tool_call), resp in zip(flattened, tool_responses):\n collected_responses[i].append(\n ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n )\n output_messages = []\n for i, candidate in enumerate(new_candidates):\n output_messages.append([candidate] + collected_responses[i])\n\n # Reflect on each candidate\n # For tasks with external validation, you'd add that here.\n reflections = reflection_chain.batch(\n [{\"input\": state[\"input\"], \"candidate\": msges} for msges in output_messages],\n config,\n )\n # Grow tree\n child_nodes = [\n Node(cand, parent=best_candidate, reflection=reflection)\n for cand, reflection in zip(output_messages, reflections)\n ]\n best_candidate.children.extend(child_nodes)\n # We have already extended the tree directly, so we just return the state\n return state"] }, { "cell_type": "markdown", @@ -636,41 +280,7 @@ "id": "8aec0f20-f978-4df0-8900-e3a1f0544f6d", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "\n", - "def should_loop(state: TreeState) -> Literal[\"expand\", \"__end__\"]:\n", - " \"\"\"Determine whether to continue the tree search.\"\"\"\n", - " root = state[\"root\"]\n", - " if root.is_solved:\n", - " return END\n", - " if root.height > 5:\n", - " return END\n", - " return \"expand\"\n", - "\n", - "\n", - "builder = StateGraph(TreeState)\n", - "builder.add_node(\"start\", generate_initial_response)\n", - "builder.add_node(\"expand\", expand)\n", - "builder.set_entry_point(\"start\")\n", - "\n", - "\n", - "builder.add_conditional_edges(\n", - " \"start\",\n", - " # Either expand/rollout or finish\n", - " should_loop,\n", - ")\n", - "builder.add_conditional_edges(\n", - " \"expand\",\n", - " # Either continue to rollout or finish\n", - " should_loop,\n", - ")\n", - "\n", - "graph = builder.compile()" - ] + "source": ["from typing import Literal\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef should_loop(state: TreeState) -> Literal[\"expand\", \"__end__\"]:\n \"\"\"Determine whether to continue the tree search.\"\"\"\n root = state[\"root\"]\n if root.is_solved:\n return END\n if root.height > 5:\n return END\n return \"expand\"\n\n\nbuilder = StateGraph(TreeState)\nbuilder.add_node(\"start\", generate_initial_response)\nbuilder.add_node(\"expand\", expand)\nbuilder.add_edge(START, \"start\")\n\n\nbuilder.add_conditional_edges(\n \"start\",\n # Either expand/rollout or finish\n should_loop,\n)\nbuilder.add_conditional_edges(\n \"expand\",\n # Either continue to rollout or finish\n should_loop,\n)\n\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -690,11 +300,7 @@ "output_type": "execute_result" } ], - "source": [ - "from IPython.display import Image\n", - "\n", - "Image(graph.get_graph().draw_mermaid_png())" - ] + "source": ["from IPython.display import Image\n\nImage(graph.get_graph().draw_mermaid_png())"] }, { "cell_type": "markdown", @@ -723,16 +329,7 @@ ] } ], - "source": [ - "question = \"Generate a table with the average size and weight, as well as the oldest recorded instance for each of the top 5 most common birds.\"\n", - "last_step = None\n", - "for step in graph.stream({\"input\": question}):\n", - " last_step = step\n", - " step_name, step_state = next(iter(step.items()))\n", - " print(step_name)\n", - " print(\"rolled out: \", step_state[\"root\"].height)\n", - " print(\"---\")" - ] + "source": ["question = \"Generate a table with the average size and weight, as well as the oldest recorded instance for each of the top 5 most common birds.\"\nlast_step = None\nfor step in graph.stream({\"input\": question}):\n last_step = step\n step_name, step_state = next(iter(step.items()))\n print(step_name)\n print(\"rolled out: \", step_state[\"root\"].height)\n print(\"---\")"] }, { "cell_type": "code", @@ -786,11 +383,7 @@ ] } ], - "source": [ - "solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\n", - "best_trajectory = solution_node.get_trajectory(include_reflections=False)\n", - "print(best_trajectory[-1].content)" - ] + "source": ["solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\nbest_trajectory = solution_node.get_trajectory(include_reflections=False)\nprint(best_trajectory[-1].content)"] }, { "cell_type": "code", @@ -814,16 +407,7 @@ ] } ], - "source": [ - "question = \"Write out magnus carlson series of moves in his game against Alireza Firouzja and propose an alternate strategy\"\n", - "last_step = None\n", - "for step in graph.stream({\"input\": question}):\n", - " last_step = step\n", - " step_name, step_state = next(iter(step.items()))\n", - " print(step_name)\n", - " print(\"rolled out: \", step_state[\"root\"].height)\n", - " print(\"---\")" - ] + "source": ["question = \"Write out magnus carlson series of moves in his game against Alireza Firouzja and propose an alternate strategy\"\nlast_step = None\nfor step in graph.stream({\"input\": question}):\n last_step = step\n step_name, step_state = next(iter(step.items()))\n print(step_name)\n print(\"rolled out: \", step_state[\"root\"].height)\n print(\"---\")"] }, { "cell_type": "code", @@ -886,11 +470,7 @@ ] } ], - "source": [ - "solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\n", - "best_trajectory = solution_node.get_trajectory(include_reflections=False)\n", - "print(best_trajectory[-1].content)" - ] + "source": ["solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\nbest_trajectory = solution_node.get_trajectory(include_reflections=False)\nprint(best_trajectory[-1].content)"] }, { "cell_type": "markdown", diff --git a/examples/learning.ipynb b/examples/learning.ipynb index c499cd056..2588a7d28 100644 --- a/examples/learning.ipynb +++ b/examples/learning.ipynb @@ -38,9 +38,7 @@ ] } ], - "source": [ - "!%pip install --quiet -U langgraph langchain langchain_openai tavily-pythonvily-python" - ] + "source": ["!%pip install --quiet -U langgraph langchain langchain_openai tavily-pythonvily-python"] }, { "cell_type": "markdown", @@ -65,13 +63,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n", - "os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")" - ] + "source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"] }, { "cell_type": "markdown", @@ -87,10 +79,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -110,11 +99,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=1)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"] }, { "cell_type": "markdown", @@ -131,11 +116,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -159,11 +140,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -180,9 +157,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -217,17 +192,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " last_message = state[\"messages\"][-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"" - ] + "source": ["# Define the function that determines whether to continue or not\ndef should_continue(state):\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -245,107 +210,7 @@ "id": "812b4e70-4956-4415-8880-db48b3dcbad2", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated, TypedDict\n", - "\n", - "from langchain_core.messages import (\n", - " AIMessage,\n", - " AnyMessage,\n", - " HumanMessage,\n", - " SystemMessage,\n", - " ToolMessage,\n", - ")\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.managed.few_shot import FewShotExamples\n", - "\n", - "\n", - "class BaseState(TypedDict):\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " examples: Annotated[list, FewShotExamples]\n", - "\n", - "\n", - "def _render_message(m):\n", - " if isinstance(m, HumanMessage):\n", - " return \"Human: \" + m.content\n", - " elif isinstance(m, AIMessage):\n", - " _m = \"AI: \" + m.content\n", - " if len(m.tool_calls) > 0:\n", - " _m += f\" Tools: {m.tool_calls}\"\n", - " return _m\n", - " elif isinstance(m, ToolMessage):\n", - " return \"Tool Result: ...\"\n", - " else:\n", - " raise ValueError\n", - "\n", - "\n", - "def _render_messages(ms):\n", - " m_string = [_render_message(m) for m in ms]\n", - " return \"\\n\".join(m_string)\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(BaseState)\n", - "\n", - "\n", - "def _agent(state: BaseState):\n", - " if len(state[\"examples\"]) > 0:\n", - " _examples = \"\\n\\n\".join(\n", - " [\n", - " f\"Example {i}: \" + _render_messages(e[\"messages\"])\n", - " for i, e in enumerate(state[\"examples\"])\n", - " ]\n", - " )\n", - " system_message = \"\"\"You are a helpful assistant. Below are some examples of interactions you had with users. \\\n", - "These were good interactions where the final result they got was the desired one. As much as possible, you should learn from these interactions and mimic them in the future. \\\n", - "Pay particularly close attention to when tools are called, and what the inputs are.!\n", - "\n", - "{examples}\n", - "\n", - "Assist the user as they require!\"\"\".format(\n", - " examples=_examples\n", - " )\n", - "\n", - " else:\n", - " system_message = \"\"\"You are a helpful assistant\"\"\"\n", - " output = model.invoke([SystemMessage(content=system_message)] + state[\"messages\"])\n", - " return {\"messages\": [output]}\n", - "\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", _agent)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")" - ] + "source": ["from typing import Annotated, TypedDict\n\nfrom langchain_core.messages import (\n AIMessage,\n AnyMessage,\n HumanMessage,\n SystemMessage,\n ToolMessage,\n)\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.managed.few_shot import FewShotExamples\n\n\nclass BaseState(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n examples: Annotated[list, FewShotExamples]\n\n\ndef _render_message(m):\n if isinstance(m, HumanMessage):\n return \"Human: \" + m.content\n elif isinstance(m, AIMessage):\n _m = \"AI: \" + m.content\n if len(m.tool_calls) > 0:\n _m += f\" Tools: {m.tool_calls}\"\n return _m\n elif isinstance(m, ToolMessage):\n return \"Tool Result: ...\"\n else:\n raise ValueError\n\n\ndef _render_messages(ms):\n m_string = [_render_message(m) for m in ms]\n return \"\\n\".join(m_string)\n\n\n# Define a new graph\nworkflow = StateGraph(BaseState)\n\n\ndef _agent(state: BaseState):\n if len(state[\"examples\"]) > 0:\n _examples = \"\\n\\n\".join(\n [\n f\"Example {i}: \" + _render_messages(e[\"messages\"])\n for i, e in enumerate(state[\"examples\"])\n ]\n )\n system_message = \"\"\"You are a helpful assistant. Below are some examples of interactions you had with users. \\\nThese were good interactions where the final result they got was the desired one. As much as possible, you should learn from these interactions and mimic them in the future. \\\nPay particularly close attention to when tools are called, and what the inputs are.!\n\n{examples}\n\nAssist the user as they require!\"\"\".format(\n examples=_examples\n )\n\n else:\n system_message = \"\"\"You are a helpful assistant\"\"\"\n output = model.invoke([SystemMessage(content=system_message)] + state[\"messages\"])\n return {\"messages\": [output]}\n\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", _agent)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"] }, { "cell_type": "markdown", @@ -363,11 +228,7 @@ "id": "6845ed6a-d155-4105-9160-28849877248b", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "code", @@ -375,12 +236,7 @@ "id": "79d29875-8aa8-434c-9f20-1c58346a6249", "metadata": {}, "outputs": [], - "source": [ - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" - ] + "source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] }, { "cell_type": "markdown", @@ -408,11 +264,7 @@ "output_type": "execute_result" } ], - "source": [ - "from IPython.display import Image\n", - "\n", - "Image(app.get_graph().draw_png())" - ] + "source": ["from IPython.display import Image\n\nImage(app.get_graph().draw_png())"] }, { "cell_type": "markdown", @@ -438,14 +290,7 @@ ] } ], - "source": [ - "thread = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "for event in app.stream(\n", - " {\"messages\": [HumanMessage(content=\"what's the weather in sf?\")]}, thread\n", - "):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["thread = {\"configurable\": {\"thread_id\": \"1\"}}\nfor event in app.stream(\n {\"messages\": [HumanMessage(content=\"what's the weather in sf?\")]}, thread\n):\n for v in event.values():\n print(v)"] }, { "cell_type": "code", @@ -465,10 +310,7 @@ "output_type": "execute_result" } ], - "source": [ - "current_values = app.get_state(thread)\n", - "current_values.values" - ] + "source": ["current_values = app.get_state(thread)\ncurrent_values.values"] }, { "cell_type": "code", @@ -476,11 +318,7 @@ "id": "1a0cdb78-40c6-4550-8c27-8f1b02d9e678", "metadata": {}, "outputs": [], - "source": [ - "current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n", - " \"query\"\n", - "] = \"weather in San Francisco, Accuweather\"" - ] + "source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco, Accuweather\""] }, { "cell_type": "code", @@ -500,9 +338,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.update_state(thread, current_values.values)" - ] + "source": ["app.update_state(thread, current_values.values)"] }, { "cell_type": "code", @@ -521,9 +357,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.get_state(thread)" - ] + "source": ["app.get_state(thread)"] }, { "cell_type": "code", @@ -540,11 +374,7 @@ ] } ], - "source": [ - "for event in app.stream(None, thread):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["for event in app.stream(None, thread):\n for v in event.values():\n print(v)"] }, { "cell_type": "code", @@ -552,16 +382,7 @@ "id": "84748206-975e-4a33-a178-d43df683298c", "metadata": {}, "outputs": [], - "source": [ - "chkpnt_tuple = memory.get_tuple({\"configurable\": {\"thread_id\": \"1\"}})\n", - "config = chkpnt_tuple.config\n", - "checkpoint = chkpnt_tuple.checkpoint\n", - "metadata = chkpnt_tuple.metadata\n", - "\n", - "# mark as \"good\"\n", - "metadata[\"score\"] = 1\n", - "memory.put(config, checkpoint, metadata)" - ] + "source": ["chkpnt_tuple = memory.get_tuple({\"configurable\": {\"thread_id\": \"1\"}})\nconfig = chkpnt_tuple.config\ncheckpoint = chkpnt_tuple.checkpoint\nmetadata = chkpnt_tuple.metadata\n\n# mark as \"good\"\nmetadata[\"score\"] = 1\nmemory.put(config, checkpoint, metadata)"] }, { "cell_type": "code", @@ -569,9 +390,7 @@ "id": "ce7fa228-8c37-4001-afd4-0001b268e1db", "metadata": {}, "outputs": [], - "source": [ - "examples = list(memory.search({\"score\": 1}))" - ] + "source": ["examples = list(memory.search({\"score\": 1}))"] }, { "cell_type": "code", @@ -590,9 +409,7 @@ "output_type": "execute_result" } ], - "source": [ - "examples" - ] + "source": ["examples"] }, { "cell_type": "code", @@ -608,14 +425,7 @@ ] } ], - "source": [ - "thread = {\"configurable\": {\"thread_id\": \"7\"}}\n", - "for event in app.stream(\n", - " {\"messages\": [HumanMessage(content=\"what's the weather in la?\")]}, thread\n", - "):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["thread = {\"configurable\": {\"thread_id\": \"7\"}}\nfor event in app.stream(\n {\"messages\": [HumanMessage(content=\"what's the weather in la?\")]}, thread\n):\n for v in event.values():\n print(v)"] }, { "cell_type": "code", @@ -632,11 +442,7 @@ ] } ], - "source": [ - "for event in app.stream(None, thread):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["for event in app.stream(None, thread):\n for v in event.values():\n print(v)"] }, { "cell_type": "code", @@ -644,7 +450,7 @@ "id": "9ab115de-9b11-4e8b-8ace-c23e1369300b", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index daf237bfc..1b9387e15 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -32,9 +32,7 @@ "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", @@ -42,22 +40,7 @@ "id": "abbd6948-e9a3-47ca-89c7-7ac2fc5eca8b", "metadata": {}, "outputs": [], - "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\")" - ] + "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\")"] }, { "cell_type": "markdown", @@ -77,23 +60,7 @@ "id": "e7476bb2-1a51-42f6-b7ae-82a0300bbf84", "metadata": {}, "outputs": [], - "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]" - ] + "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]"] }, { "cell_type": "code", @@ -112,14 +79,7 @@ "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", @@ -188,26 +148,7 @@ ] } ], - "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())" - ] + "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())"] }, { "cell_type": "code", @@ -215,58 +156,7 @@ "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", @@ -274,11 +164,7 @@ "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\n", - "planner = create_planner(llm, tools, prompt)" - ] + "source": ["llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n# This is the primary \"agent\" in our application\nplanner = create_planner(llm, tools, prompt)"] }, { "cell_type": "code", @@ -299,13 +185,7 @@ ] } ], - "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(\"---\")" - ] + "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(\"---\")"] }, { "cell_type": "markdown", @@ -337,168 +217,7 @@ "jp-MarkdownHeadingCollapsed": true }, "outputs": [], - "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" - ] + "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"] }, { "cell_type": "code", @@ -506,28 +225,7 @@ "id": "052f6b16-103a-40e9-94dd-8fcc37e77ba4", "metadata": {}, "outputs": [], - "source": [ - "import itertools\n", - "\n", - "\n", - "@as_runnable\n", - "def 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\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"] }, { "cell_type": "markdown", @@ -545,9 +243,7 @@ "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", @@ -568,9 +264,7 @@ "output_type": "execute_result" } ], - "source": [ - "tool_messages" - ] + "source": ["tool_messages"] }, { "cell_type": "markdown", @@ -593,40 +287,7 @@ "id": "942dab42-ad42-4ba2-90d5-49edbe4fae68", "metadata": {}, "outputs": [], - "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)" - ] + "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)"] }, { "cell_type": "markdown", @@ -643,30 +304,7 @@ "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", - "\n", - "def 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", - "\n", - "joiner = 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 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"] }, { "cell_type": "code", @@ -674,9 +312,7 @@ "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", @@ -696,9 +332,7 @@ "output_type": "execute_result" } ], - "source": [ - "joiner.invoke(input_messages)" - ] + "source": ["joiner.invoke(input_messages)"] }, { "cell_type": "markdown", @@ -720,40 +354,7 @@ "id": "768b5f11-e3d2-47be-8143-a7dcd8765243", "metadata": {}, "outputs": [], - "source": [ - "from typing import Dict\n", - "\n", - "from langgraph.graph import END, MessageGraph\n", - "\n", - "graph_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\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: List[BaseMessage]):\n", - " if isinstance(state[-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.set_entry_point(\"plan_and_schedule\")\n", - "chain = graph_builder.compile()" - ] + "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()"] }, { "cell_type": "markdown", @@ -788,11 +389,7 @@ ] } ], - "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([HumanMessage(content=\"What's the GDP of New York?\")]):\n print(step)\n print(\"---\")"] }, { "cell_type": "code", @@ -808,10 +405,7 @@ ] } ], - "source": [ - "# Final answer\n", - "print(step[END][-1].content)" - ] + "source": ["# Final answer\nprint(step[END][-1].content)"] }, { "cell_type": "markdown", @@ -846,21 +440,7 @@ ] } ], - "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(\"---\")" - ] + "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(\"---\")"] }, { "cell_type": "code", @@ -876,10 +456,7 @@ ] } ], - "source": [ - "# Final answer\n", - "print(step[END][-1].content)" - ] + "source": ["# Final answer\nprint(step[END][-1].content)"] }, { "cell_type": "markdown", @@ -905,16 +482,7 @@ ] } ], - "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", @@ -932,10 +500,7 @@ ] } ], - "source": [ - "# Final answer\n", - "print(step[END][-1].content)" - ] + "source": ["# Final answer\nprint(step[END][-1].content)"] }, { "cell_type": "markdown", @@ -957,7 +522,7 @@ "id": "431217e6-4c00-409f-a2bd-40ebff902489", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/managing-agent-steps.ipynb b/examples/managing-agent-steps.ipynb index 670ed12c3..123e58c5f 100644 --- a/examples/managing-agent-steps.ipynb +++ b/examples/managing-agent-steps.ipynb @@ -28,10 +28,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"] }, { "cell_type": "markdown", @@ -47,18 +44,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -74,10 +60,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -103,22 +86,7 @@ "id": "5f374964", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# Add messages essentially does this with more\n", - "# robust handling\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -138,21 +106,7 @@ "id": "692cffb0", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder, but don't tell the LLM that...\n", - " return [\n", - " \"Try again in a few seconds! Checking with the weathermen... Call be again next.\"\n", - " ]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\n \"Try again in a few seconds! Checking with the weathermen... Call be again next.\"\n ]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -169,11 +123,7 @@ "id": "ae7abc20", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -197,11 +147,7 @@ "id": "4ad247ff", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)"] }, { "cell_type": "markdown", @@ -219,9 +165,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -256,21 +200,7 @@ "id": "e718a9c5-6596-457f-ac25-a25d8cb8c259", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state: State) -> Literal[\"__end__\", \"action\"]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"" - ] + "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"__end__\", \"action\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -288,19 +218,7 @@ "id": "714e4135-7cb5-4f17-b2ae-46f7e98bde61", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = []\n", - " for m in state[\"messages\"][::-1]:\n", - " messages.append(m)\n", - " if len(messages) >= 5:\n", - " if messages[-1].type != \"tool\":\n", - " break\n", - " response = model.invoke(messages[::-1])\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["# Define the function that calls the model\ndef call_model(state):\n messages = []\n for m in state[\"messages\"][::-1]:\n messages.append(m)\n if len(messages) >= 5:\n if messages[-1].type != \"tool\":\n break\n response = model.invoke(messages[::-1])\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "markdown", @@ -318,50 +236,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -380,11 +255,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -635,22 +506,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"what is the weather in sf? Don't give up! Keep using your tools.\"\n", - " )\n", - " ]\n", - "}\n", - "for event in app.stream(inputs, stream_mode=\"values\"):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for message in event[\"messages\"]:\n", - " message.pretty_print()\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\n \"messages\": [\n HumanMessage(\n content=\"what is the weather in sf? Don't give up! Keep using your tools.\"\n )\n ]\n}\nfor event in app.stream(inputs, stream_mode=\"values\"):\n # stream() yields dictionaries with output keyed by node name\n for message in event[\"messages\"]:\n message.pretty_print()\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -658,7 +514,7 @@ "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/managing-conversation-history.ipynb b/examples/managing-conversation-history.ipynb index 35182b053..e908cf1d5 100644 --- a/examples/managing-conversation-history.ipynb +++ b/examples/managing-conversation-history.ipynb @@ -98,7 +98,7 @@ "from langchain_core.tools import tool\n", "\n", "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import MessagesState, StateGraph\n", + "from langgraph.graph import MessagesState, StateGraph, START\n", "from langgraph.prebuilt import ToolNode\n", "\n", "memory = SqliteSaver.from_conn_string(\":memory:\")\n", @@ -146,7 +146,7 @@ "\n", "# Set the entrypoint as `agent`\n", "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", + "workflow.add_edge(START, \"agent\")\n", "\n", "# We now add a conditional edge\n", "workflow.add_conditional_edges(\n", @@ -229,7 +229,7 @@ "from langchain_core.tools import tool\n", "\n", "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import MessagesState, StateGraph\n", + "from langgraph.graph import MessagesState, StateGraph, START\n", "from langgraph.prebuilt import ToolNode\n", "\n", "memory = SqliteSaver.from_conn_string(\":memory:\")\n", @@ -283,7 +283,7 @@ "\n", "# Set the entrypoint as `agent`\n", "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", + "workflow.add_edge(START, \"agent\")\n", "\n", "# We now add a conditional edge\n", "workflow.add_conditional_edges(\n", diff --git a/examples/map-reduce.ipynb b/examples/map-reduce.ipynb index f1d81c861..0689b9886 100644 --- a/examples/map-reduce.ipynb +++ b/examples/map-reduce.ipynb @@ -34,110 +34,7 @@ ] } ], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict\n", - "\n", - "from langchain_core.pydantic_v1 import BaseModel\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "from langgraph.constants import Send\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Model and prompts\n", - "# Define model and prompts we will use\n", - "subjects_prompt = \"\"\"Generate a comma separated list of between 2 and 5 {topic}.\"\"\"\n", - "joke_prompt = \"\"\"Generate a joke about {subject}\"\"\"\n", - "best_joke_prompt = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one.\n", - "\n", - "{jokes}\"\"\"\n", - "\n", - "\n", - "class Subjects(BaseModel):\n", - " subjects: list[str]\n", - "\n", - "\n", - "class Joke(BaseModel):\n", - " joke: str\n", - "\n", - "\n", - "class BestJoke(BaseModel):\n", - " id: int\n", - "\n", - "\n", - "model = ChatOpenAI()\n", - "\n", - "# Graph components: define the components that will make up the graph\n", - "\n", - "\n", - "# This will be the overall state of the main graph.\n", - "# It will contain a topic (which we expect the user to provide)\n", - "# and then will generate a list of subjects, and then a joke for\n", - "# each subject\n", - "class OverallState(TypedDict):\n", - " topic: str\n", - " subjects: list\n", - " # Notice here we use the operator.add\n", - " # This is because we want combine all the jokes we generate\n", - " # from individual nodes back into one list - this is essentially\n", - " # the \"reduce\" part\n", - " jokes: Annotated[list, operator.add]\n", - " best_selected_joke: str\n", - "\n", - "\n", - "# This will be the state of the node that we will \"map\" all\n", - "# subjects to in order to generate a joke\n", - "class JokeState(TypedDict):\n", - " subject: str\n", - "\n", - "\n", - "# This is the function we will use to generate the subjects of the jokes\n", - "def generate_topics(state: OverallState):\n", - " prompt = subjects_prompt.format(topic=state[\"topic\"])\n", - " response = model.with_structured_output(Subjects).invoke(prompt)\n", - " return {\"subjects\": response.subjects}\n", - "\n", - "\n", - "# Here we generate a joke, given a subject\n", - "def generate_joke(state: JokeState):\n", - " prompt = joke_prompt.format(subject=state[\"subject\"])\n", - " response = model.with_structured_output(Joke).invoke(prompt)\n", - " return {\"jokes\": [response.joke]}\n", - "\n", - "\n", - "# Here we define the logic to map out over the generated subjects\n", - "# We will use this an edge in the graph\n", - "def continue_to_jokes(state: OverallState):\n", - " # We will return a list of `Send` objects\n", - " # Each `Send` object consists of the name of a node in the graph\n", - " # as well as the state to send to that node\n", - " return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]\n", - "\n", - "\n", - "# Here we will judge the best joke\n", - "def best_joke(state: OverallState):\n", - " jokes = \"\\n\\n\".format()\n", - " prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n", - " response = model.with_structured_output(BestJoke).invoke(prompt)\n", - " return {\"best_selected_joke\": state[\"jokes\"][response.id]}\n", - "\n", - "\n", - "# Construct the graph: here we put everything together to construct our graph\n", - "graph = StateGraph(OverallState)\n", - "graph.add_node(\"generate_topics\", generate_topics)\n", - "graph.add_node(\"generate_joke\", generate_joke)\n", - "graph.add_node(\"best_joke\", best_joke)\n", - "graph.set_entry_point(\"generate_topics\")\n", - "graph.add_conditional_edges(\"generate_topics\", continue_to_jokes)\n", - "graph.add_edge(\"generate_joke\", \"best_joke\")\n", - "graph.add_edge(\"best_joke\", END)\n", - "app = graph.compile()\n", - "\n", - "\n", - "# Call the graph: here we call it to generate a list of jokes\n", - "for s in app.stream({\"topic\": \"animals\"}):\n", - " print(s)" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict\n\nfrom langchain_core.pydantic_v1 import BaseModel\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.constants import Send\nfrom langgraph.graph import END, StateGraph, START\n\n# Model and prompts\n# Define model and prompts we will use\nsubjects_prompt = \"\"\"Generate a comma separated list of between 2 and 5 {topic}.\"\"\"\njoke_prompt = \"\"\"Generate a joke about {subject}\"\"\"\nbest_joke_prompt = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one.\n\n{jokes}\"\"\"\n\n\nclass Subjects(BaseModel):\n subjects: list[str]\n\n\nclass Joke(BaseModel):\n joke: str\n\n\nclass BestJoke(BaseModel):\n id: int\n\n\nmodel = ChatOpenAI()\n\n# Graph components: define the components that will make up the graph\n\n\n# This will be the overall state of the main graph.\n# It will contain a topic (which we expect the user to provide)\n# and then will generate a list of subjects, and then a joke for\n# each subject\nclass OverallState(TypedDict):\n topic: str\n subjects: list\n # Notice here we use the operator.add\n # This is because we want combine all the jokes we generate\n # from individual nodes back into one list - this is essentially\n # the \"reduce\" part\n jokes: Annotated[list, operator.add]\n best_selected_joke: str\n\n\n# This will be the state of the node that we will \"map\" all\n# subjects to in order to generate a joke\nclass JokeState(TypedDict):\n subject: str\n\n\n# This is the function we will use to generate the subjects of the jokes\ndef generate_topics(state: OverallState):\n prompt = subjects_prompt.format(topic=state[\"topic\"])\n response = model.with_structured_output(Subjects).invoke(prompt)\n return {\"subjects\": response.subjects}\n\n\n# Here we generate a joke, given a subject\ndef generate_joke(state: JokeState):\n prompt = joke_prompt.format(subject=state[\"subject\"])\n response = model.with_structured_output(Joke).invoke(prompt)\n return {\"jokes\": [response.joke]}\n\n\n# Here we define the logic to map out over the generated subjects\n# We will use this an edge in the graph\ndef continue_to_jokes(state: OverallState):\n # We will return a list of `Send` objects\n # Each `Send` object consists of the name of a node in the graph\n # as well as the state to send to that node\n return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]\n\n\n# Here we will judge the best joke\ndef best_joke(state: OverallState):\n jokes = \"\\n\\n\".format()\n prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n response = model.with_structured_output(BestJoke).invoke(prompt)\n return {\"best_selected_joke\": state[\"jokes\"][response.id]}\n\n\n# Construct the graph: here we put everything together to construct our graph\ngraph = StateGraph(OverallState)\ngraph.add_node(\"generate_topics\", generate_topics)\ngraph.add_node(\"generate_joke\", generate_joke)\ngraph.add_node(\"best_joke\", best_joke)\ngraph.add_edge(START, \"generate_topics\")\ngraph.add_conditional_edges(\"generate_topics\", continue_to_jokes)\ngraph.add_edge(\"generate_joke\", \"best_joke\")\ngraph.add_edge(\"best_joke\", END)\napp = graph.compile()\n\n\n# Call the graph: here we call it to generate a list of jokes\nfor s in app.stream({\"topic\": \"animals\"}):\n print(s)"] }, { "cell_type": "code", @@ -145,7 +42,7 @@ "id": "37ed1f71-63db-416f-b715-4617b33d4b7f", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/multi_agent/agent_supervisor.ipynb b/examples/multi_agent/agent_supervisor.ipynb index 3b84dc8f1..c6626d755 100644 --- a/examples/multi_agent/agent_supervisor.ipynb +++ b/examples/multi_agent/agent_supervisor.ipynb @@ -26,10 +26,7 @@ "id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"] }, { "cell_type": "code", @@ -37,24 +34,7 @@ "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", "metadata": {}, "outputs": [], - "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", - "_set_if_undefined(\"TAVILY_API_KEY\")\n", - "\n", - "# Optional, add tracing in LangSmith\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" - ] + "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_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] }, { "cell_type": "markdown", @@ -72,17 +52,7 @@ "id": "f04c6778-403b-4b49-9b93-678e910d5cec", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_experimental.tools import PythonREPLTool\n", - "\n", - "tavily_tool = TavilySearchResults(max_results=5)\n", - "\n", - "# This executes code locally, which can be unsafe\n", - "python_repl_tool = PythonREPLTool()" - ] + "source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_experimental.tools import PythonREPLTool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# This executes code locally, which can be unsafe\npython_repl_tool = PythonREPLTool()"] }, { "cell_type": "markdown", @@ -100,28 +70,7 @@ "id": "c4823dd9-26bd-4e1a-8117-b97b2860211a", "metadata": {}, "outputs": [], - "source": [ - "from langchain.agents import AgentExecutor, create_openai_tools_agent\n", - "from langchain_core.messages import BaseMessage, HumanMessage\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "\n", - "def create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n", - " # Each worker node will be given a name and some tools.\n", - " prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " system_prompt,\n", - " ),\n", - " MessagesPlaceholder(variable_name=\"messages\"),\n", - " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", - " ]\n", - " )\n", - " agent = create_openai_tools_agent(llm, tools, prompt)\n", - " executor = AgentExecutor(agent=agent, tools=tools)\n", - " return executor" - ] + "source": ["from langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\n\ndef create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n # Each worker node will be given a name and some tools.\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_tools_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor"] }, { "cell_type": "markdown", @@ -137,11 +86,7 @@ "id": "80862241-a1a7-4726-bce5-f867b233832e", "metadata": {}, "outputs": [], - "source": [ - "def agent_node(state, agent, name):\n", - " result = agent.invoke(state)\n", - " return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}" - ] + "source": ["def agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}"] }, { "cell_type": "markdown", @@ -159,59 +104,7 @@ "id": "311f0a58-b425-4496-adac-dc4cd8ffb912", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\n", - "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", - "\n", - "members = [\"Researcher\", \"Coder\"]\n", - "system_prompt = (\n", - " \"You are a supervisor tasked with managing a conversation between the\"\n", - " \" following workers: {members}. Given the following user request,\"\n", - " \" respond with the worker to act next. Each worker will perform a\"\n", - " \" task and respond with their results and status. When finished,\"\n", - " \" respond with FINISH.\"\n", - ")\n", - "# Our team supervisor is an LLM node. It just picks the next agent to process\n", - "# and decides when the work is completed\n", - "options = [\"FINISH\"] + members\n", - "# Using openai function calling can make output parsing easier for us\n", - "function_def = {\n", - " \"name\": \"route\",\n", - " \"description\": \"Select the next role.\",\n", - " \"parameters\": {\n", - " \"title\": \"routeSchema\",\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"next\": {\n", - " \"title\": \"Next\",\n", - " \"anyOf\": [\n", - " {\"enum\": options},\n", - " ],\n", - " }\n", - " },\n", - " \"required\": [\"next\"],\n", - " },\n", - "}\n", - "prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system_prompt),\n", - " MessagesPlaceholder(variable_name=\"messages\"),\n", - " (\n", - " \"system\",\n", - " \"Given the conversation above, who should act next?\"\n", - " \" Or should we FINISH? Select one of: {options}\",\n", - " ),\n", - " ]\n", - ").partial(options=str(options), members=\", \".join(members))\n", - "\n", - "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", - "\n", - "supervisor_chain = (\n", - " prompt\n", - " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", - " | JsonOutputFunctionsParser()\n", - ")" - ] + "source": ["from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nmembers = [\"Researcher\", \"Coder\"]\nsystem_prompt = (\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\"\n)\n# Our team supervisor is an LLM node. It just picks the next agent to process\n# and decides when the work is completed\noptions = [\"FINISH\"] + members\n# Using openai function calling can make output parsing easier for us\nfunction_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n }\n },\n \"required\": [\"next\"],\n },\n}\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n).partial(options=str(options), members=\", \".join(members))\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_chain = (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n)"] }, { "cell_type": "markdown", @@ -229,41 +122,7 @@ "id": "6a430af7-8fce-4e66-ba9e-d940c1bc48e8", "metadata": {}, "outputs": [], - "source": [ - "import functools\n", - "import operator\n", - "from typing import Sequence, TypedDict\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "\n", - "# The agent state is the input to each node in the graph\n", - "class AgentState(TypedDict):\n", - " # The annotation tells the graph that new messages will always\n", - " # be added to the current states\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]\n", - " # The 'next' field indicates where to route to next\n", - " next: str\n", - "\n", - "\n", - "research_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\n", - "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n", - "\n", - "# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\n", - "code_agent = create_agent(\n", - " llm,\n", - " [python_repl_tool],\n", - " \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n", - ")\n", - "code_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n", - "\n", - "workflow = StateGraph(AgentState)\n", - "workflow.add_node(\"Researcher\", research_node)\n", - "workflow.add_node(\"Coder\", code_node)\n", - "workflow.add_node(\"supervisor\", supervisor_chain)" - ] + "source": ["import functools\nimport operator\nfrom typing import Sequence, TypedDict\n\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\n# The agent state is the input to each node in the graph\nclass AgentState(TypedDict):\n # The annotation tells the graph that new messages will always\n # be added to the current states\n messages: Annotated[Sequence[BaseMessage], operator.add]\n # The 'next' field indicates where to route to next\n next: str\n\n\nresearch_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\ncode_agent = create_agent(\n llm,\n [python_repl_tool],\n \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n)\ncode_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"Coder\", code_node)\nworkflow.add_node(\"supervisor\", supervisor_chain)"] }, { "cell_type": "markdown", @@ -279,20 +138,7 @@ "id": "14778e86-077b-4e6a-893c-400e59b0cdbf", "metadata": {}, "outputs": [], - "source": [ - "for member in members:\n", - " # We want our workers to ALWAYS \"report back\" to the supervisor when done\n", - " workflow.add_edge(member, \"supervisor\")\n", - "# The supervisor populates the \"next\" field in the graph state\n", - "# which routes to a node or finishes\n", - "conditional_map = {k: k for k in members}\n", - "conditional_map[\"FINISH\"] = END\n", - "workflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n", - "# Finally, add entrypoint\n", - "workflow.set_entry_point(\"supervisor\")\n", - "\n", - "graph = workflow.compile()" - ] + "source": ["for member in members:\n # We want our workers to ALWAYS \"report back\" to the supervisor when done\n workflow.add_edge(member, \"supervisor\")\n# The supervisor populates the \"next\" field in the graph state\n# which routes to a node or finishes\nconditional_map = {k: k for k in members}\nconditional_map[\"FINISH\"] = END\nworkflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n# Finally, add entrypoint\nworkflow.add_edge(START, \"supervisor\")\n\ngraph = workflow.compile()"] }, { "cell_type": "markdown", @@ -336,18 +182,7 @@ ] } ], - "source": [ - "for s in graph.stream(\n", - " {\n", - " \"messages\": [\n", - " HumanMessage(content=\"Code hello world and print it to the terminal\")\n", - " ]\n", - " }\n", - "):\n", - " if \"__end__\" not in s:\n", - " print(s)\n", - " print(\"----\")" - ] + "source": ["for s in graph.stream(\n {\n \"messages\": [\n HumanMessage(content=\"Code hello world and print it to the terminal\")\n ]\n }\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"] }, { "cell_type": "code", @@ -368,15 +203,7 @@ ] } ], - "source": [ - "for s in graph.stream(\n", - " {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n", - " {\"recursion_limit\": 100},\n", - "):\n", - " if \"__end__\" not in s:\n", - " print(s)\n", - " print(\"----\")" - ] + "source": ["for s in graph.stream(\n {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"] }, { "cell_type": "code", @@ -384,7 +211,7 @@ "id": "1d363d2c-e0da-4cce-ba47-ad2aa9df0fef", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/multi_agent/hierarchical_agent_teams.ipynb b/examples/multi_agent/hierarchical_agent_teams.ipynb index bb9609df3..e539c91f9 100644 --- a/examples/multi_agent/hierarchical_agent_teams.ipynb +++ b/examples/multi_agent/hierarchical_agent_teams.ipynb @@ -40,10 +40,7 @@ } }, "outputs": [], - "source": [ - "# %%capture --no-stderr\n", - "# %pip install -U langgraph langchain langchain_openai langchain_experimental" - ] + "source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai langchain_experimental"] }, { "cell_type": "code", @@ -56,25 +53,7 @@ } }, "outputs": [], - "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", - "_set_if_undefined(\"TAVILY_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\"] = \"Multi-agent Collaboration\"" - ] + "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_set_if_undefined(\"TAVILY_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\"] = \"Multi-agent Collaboration\""] }, { "cell_type": "markdown", @@ -103,28 +82,7 @@ } }, "outputs": [], - "source": [ - "from typing import Annotated, List\n", - "\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.tools import tool\n", - "\n", - "tavily_tool = TavilySearchResults(max_results=5)\n", - "\n", - "\n", - "@tool\n", - "def scrape_webpages(urls: List[str]) -> str:\n", - " \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n", - " loader = WebBaseLoader(urls)\n", - " docs = loader.load()\n", - " return \"\\n\\n\".join(\n", - " [\n", - " f'\\n{doc.page_content}\\n'\n", - " for doc in docs\n", - " ]\n", - " )" - ] + "source": ["from typing import Annotated, List\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n\n@tool\ndef scrape_webpages(urls: List[str]) -> str:\n \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n loader = WebBaseLoader(urls)\n docs = loader.load()\n return \"\\n\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )"] }, { "cell_type": "markdown", @@ -150,99 +108,7 @@ } }, "outputs": [], - "source": [ - "from pathlib import Path\n", - "from tempfile import TemporaryDirectory\n", - "from typing import Dict, Optional\n", - "\n", - "from langchain_experimental.utilities import PythonREPL\n", - "from typing_extensions import TypedDict\n", - "\n", - "_TEMP_DIRECTORY = TemporaryDirectory()\n", - "WORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n", - "\n", - "\n", - "@tool\n", - "def create_outline(\n", - " points: Annotated[List[str], \"List of main points or sections.\"],\n", - " file_name: Annotated[str, \"File path to save the outline.\"],\n", - ") -> Annotated[str, \"Path of the saved outline file.\"]:\n", - " \"\"\"Create and save an outline.\"\"\"\n", - " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", - " for i, point in enumerate(points):\n", - " file.write(f\"{i + 1}. {point}\\n\")\n", - " return f\"Outline saved to {file_name}\"\n", - "\n", - "\n", - "@tool\n", - "def read_document(\n", - " file_name: Annotated[str, \"File path to save the document.\"],\n", - " start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n", - " end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n", - ") -> str:\n", - " \"\"\"Read the specified document.\"\"\"\n", - " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", - " lines = file.readlines()\n", - " if start is not None:\n", - " start = 0\n", - " return \"\\n\".join(lines[start:end])\n", - "\n", - "\n", - "@tool\n", - "def write_document(\n", - " content: Annotated[str, \"Text content to be written into the document.\"],\n", - " file_name: Annotated[str, \"File path to save the document.\"],\n", - ") -> Annotated[str, \"Path of the saved document file.\"]:\n", - " \"\"\"Create and save a text document.\"\"\"\n", - " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", - " file.write(content)\n", - " return f\"Document saved to {file_name}\"\n", - "\n", - "\n", - "@tool\n", - "def edit_document(\n", - " file_name: Annotated[str, \"Path of the document to be edited.\"],\n", - " inserts: Annotated[\n", - " Dict[int, str],\n", - " \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n", - " ],\n", - ") -> Annotated[str, \"Path of the edited document file.\"]:\n", - " \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n", - "\n", - " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", - " lines = file.readlines()\n", - "\n", - " sorted_inserts = sorted(inserts.items())\n", - "\n", - " for line_number, text in sorted_inserts:\n", - " if 1 <= line_number <= len(lines) + 1:\n", - " lines.insert(line_number - 1, text + \"\\n\")\n", - " else:\n", - " return f\"Error: Line number {line_number} is out of range.\"\n", - "\n", - " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", - " file.writelines(lines)\n", - "\n", - " return f\"Document edited and saved to {file_name}\"\n", - "\n", - "\n", - "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", - "\n", - "repl = PythonREPL()\n", - "\n", - "\n", - "@tool\n", - "def python_repl(\n", - " code: Annotated[str, \"The python code to execute to generate your chart.\"],\n", - "):\n", - " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", - " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", - " try:\n", - " result = repl.run(code)\n", - " except BaseException as e:\n", - " return f\"Failed to execute. Error: {repr(e)}\"\n", - " return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"" - ] + "source": ["from pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Dict, Optional\n\nfrom langchain_experimental.utilities import PythonREPL\nfrom typing_extensions import TypedDict\n\n_TEMP_DIRECTORY = TemporaryDirectory()\nWORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n\n\n@tool\ndef create_outline(\n points: Annotated[List[str], \"List of main points or sections.\"],\n file_name: Annotated[str, \"File path to save the outline.\"],\n) -> Annotated[str, \"Path of the saved outline file.\"]:\n \"\"\"Create and save an outline.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n for i, point in enumerate(points):\n file.write(f\"{i + 1}. {point}\\n\")\n return f\"Outline saved to {file_name}\"\n\n\n@tool\ndef read_document(\n file_name: Annotated[str, \"File path to save the document.\"],\n start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n) -> str:\n \"\"\"Read the specified document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n if start is not None:\n start = 0\n return \"\\n\".join(lines[start:end])\n\n\n@tool\ndef write_document(\n content: Annotated[str, \"Text content to be written into the document.\"],\n file_name: Annotated[str, \"File path to save the document.\"],\n) -> Annotated[str, \"Path of the saved document file.\"]:\n \"\"\"Create and save a text document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.write(content)\n return f\"Document saved to {file_name}\"\n\n\n@tool\ndef edit_document(\n file_name: Annotated[str, \"Path of the document to be edited.\"],\n inserts: Annotated[\n Dict[int, str],\n \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n ],\n) -> Annotated[str, \"Path of the edited document file.\"]:\n \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n\n sorted_inserts = sorted(inserts.items())\n\n for line_number, text in sorted_inserts:\n if 1 <= line_number <= len(lines) + 1:\n lines.insert(line_number - 1, text + \"\\n\")\n else:\n return f\"Error: Line number {line_number} is out of range.\"\n\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.writelines(lines)\n\n return f\"Document edited and saved to {file_name}\"\n\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\""] }, { "cell_type": "markdown", @@ -270,84 +136,7 @@ } }, "outputs": [], - "source": [ - "from typing import List, Optional\n", - "\n", - "from langchain.agents import AgentExecutor, create_openai_functions_agent\n", - "from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n", - "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "\n", - "def create_agent(\n", - " llm: ChatOpenAI,\n", - " tools: list,\n", - " system_prompt: str,\n", - ") -> str:\n", - " \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n", - " system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n", - " \" Do not ask for clarification.\"\n", - " \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n", - " \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n", - " prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " system_prompt,\n", - " ),\n", - " MessagesPlaceholder(variable_name=\"messages\"),\n", - " MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n", - " ]\n", - " )\n", - " agent = create_openai_functions_agent(llm, tools, prompt)\n", - " executor = AgentExecutor(agent=agent, tools=tools)\n", - " return executor\n", - "\n", - "\n", - "def agent_node(state, agent, name):\n", - " result = agent.invoke(state)\n", - " return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n", - "\n", - "\n", - "def create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n", - " \"\"\"An LLM-based router.\"\"\"\n", - " options = [\"FINISH\"] + members\n", - " function_def = {\n", - " \"name\": \"route\",\n", - " \"description\": \"Select the next role.\",\n", - " \"parameters\": {\n", - " \"title\": \"routeSchema\",\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"next\": {\n", - " \"title\": \"Next\",\n", - " \"anyOf\": [\n", - " {\"enum\": options},\n", - " ],\n", - " },\n", - " },\n", - " \"required\": [\"next\"],\n", - " },\n", - " }\n", - " prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system_prompt),\n", - " MessagesPlaceholder(variable_name=\"messages\"),\n", - " (\n", - " \"system\",\n", - " \"Given the conversation above, who should act next?\"\n", - " \" Or should we FINISH? Select one of: {options}\",\n", - " ),\n", - " ]\n", - " ).partial(options=str(options), team_members=\", \".join(members))\n", - " return (\n", - " prompt\n", - " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", - " | JsonOutputFunctionsParser()\n", - " )" - ] + "source": ["from typing import List, Optional\n\nfrom langchain.agents import AgentExecutor, create_openai_functions_agent\nfrom langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(\n llm: ChatOpenAI,\n tools: list,\n system_prompt: str,\n) -> str:\n \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n \" Do not ask for clarification.\"\n \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_functions_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor\n\n\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n\n\ndef create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n \"\"\"An LLM-based router.\"\"\"\n options = [\"FINISH\"] + members\n function_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n },\n },\n \"required\": [\"next\"],\n },\n }\n prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n ).partial(options=str(options), team_members=\", \".join(members))\n return (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n )"] }, { "cell_type": "markdown", @@ -374,52 +163,7 @@ } }, "outputs": [], - "source": [ - "import functools\n", - "import operator\n", - "\n", - "from langchain_core.messages import BaseMessage, HumanMessage\n", - "from langchain_openai.chat_models import ChatOpenAI\n", - "\n", - "\n", - "# ResearchTeam graph state\n", - "class ResearchTeamState(TypedDict):\n", - " # A message is added after each team member finishes\n", - " messages: Annotated[List[BaseMessage], operator.add]\n", - " # The team members are tracked so they are aware of\n", - " # the others' skill-sets\n", - " team_members: List[str]\n", - " # Used to route work. The supervisor calls a function\n", - " # that will update this every time it makes a decision\n", - " next: str\n", - "\n", - "\n", - "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", - "\n", - "search_agent = create_agent(\n", - " llm,\n", - " [tavily_tool],\n", - " \"You are a research assistant who can search for up-to-date info using the tavily search engine.\",\n", - ")\n", - "search_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n", - "\n", - "research_agent = create_agent(\n", - " llm,\n", - " [scrape_webpages],\n", - " \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\",\n", - ")\n", - "research_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n", - "\n", - "supervisor_agent = create_team_supervisor(\n", - " llm,\n", - " \"You are a supervisor tasked with managing a conversation between the\"\n", - " \" following workers: Search, WebScraper. Given the following user request,\"\n", - " \" respond with the worker to act next. Each worker will perform a\"\n", - " \" task and respond with their results and status. When finished,\"\n", - " \" respond with FINISH.\",\n", - " [\"Search\", \"WebScraper\"],\n", - ")" - ] + "source": ["import functools\nimport operator\n\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\n\n# ResearchTeam graph state\nclass ResearchTeamState(TypedDict):\n # A message is added after each team member finishes\n messages: Annotated[List[BaseMessage], operator.add]\n # The team members are tracked so they are aware of\n # the others' skill-sets\n team_members: List[str]\n # Used to route work. The supervisor calls a function\n # that will update this every time it makes a decision\n next: str\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsearch_agent = create_agent(\n llm,\n [tavily_tool],\n \"You are a research assistant who can search for up-to-date info using the tavily search engine.\",\n)\nsearch_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n\nresearch_agent = create_agent(\n llm,\n [scrape_webpages],\n \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n\nsupervisor_agent = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: Search, WebScraper. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"Search\", \"WebScraper\"],\n)"] }, { "cell_type": "markdown", @@ -440,38 +184,7 @@ } }, "outputs": [], - "source": [ - "research_graph = StateGraph(ResearchTeamState)\n", - "research_graph.add_node(\"Search\", search_node)\n", - "research_graph.add_node(\"WebScraper\", research_node)\n", - "research_graph.add_node(\"supervisor\", supervisor_agent)\n", - "\n", - "# Define the control flow\n", - "research_graph.add_edge(\"Search\", \"supervisor\")\n", - "research_graph.add_edge(\"WebScraper\", \"supervisor\")\n", - "research_graph.add_conditional_edges(\n", - " \"supervisor\",\n", - " lambda x: x[\"next\"],\n", - " {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n", - ")\n", - "\n", - "\n", - "research_graph.set_entry_point(\"supervisor\")\n", - "chain = research_graph.compile()\n", - "\n", - "\n", - "# The following functions interoperate between the top level graph state\n", - "# and the state of the research sub-graph\n", - "# this makes it so that the states of each graph don't get intermixed\n", - "def enter_chain(message: str):\n", - " results = {\n", - " \"messages\": [HumanMessage(content=message)],\n", - " }\n", - " return results\n", - "\n", - "\n", - "research_chain = enter_chain | chain" - ] + "source": ["research_graph = StateGraph(ResearchTeamState)\nresearch_graph.add_node(\"Search\", search_node)\nresearch_graph.add_node(\"WebScraper\", research_node)\nresearch_graph.add_node(\"supervisor\", supervisor_agent)\n\n# Define the control flow\nresearch_graph.add_edge(\"Search\", \"supervisor\")\nresearch_graph.add_edge(\"WebScraper\", \"supervisor\")\nresearch_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n)\n\n\nresearch_graph.add_edge(START, \"supervisor\")\nchain = research_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str):\n results = {\n \"messages\": [HumanMessage(content=message)],\n }\n return results\n\n\nresearch_chain = enter_chain | chain"] }, { "cell_type": "code", @@ -495,11 +208,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(chain.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -520,14 +229,7 @@ } }, "outputs": [], - "source": [ - "for s in research_chain.stream(\n", - " \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n", - "):\n", - " if \"__end__\" not in s:\n", - " print(s)\n", - " print(\"---\")" - ] + "source": ["for s in research_chain.stream(\n \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] }, { "cell_type": "markdown", @@ -552,92 +254,7 @@ } }, "outputs": [], - "source": [ - "import operator\n", - "from pathlib import Path\n", - "\n", - "\n", - "# Document writing team graph state\n", - "class DocWritingState(TypedDict):\n", - " # This tracks the team's conversation internally\n", - " messages: Annotated[List[BaseMessage], operator.add]\n", - " # This provides each worker with context on the others' skill sets\n", - " team_members: str\n", - " # This is how the supervisor tells langgraph who to work next\n", - " next: str\n", - " # This tracks the shared directory state\n", - " current_files: str\n", - "\n", - "\n", - "# This will be run before each worker agent begins work\n", - "# It makes it so they are more aware of the current state\n", - "# of the working directory.\n", - "def prelude(state):\n", - " written_files = []\n", - " if not WORKING_DIRECTORY.exists():\n", - " WORKING_DIRECTORY.mkdir()\n", - " try:\n", - " written_files = [\n", - " f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n", - " ]\n", - " except Exception:\n", - " pass\n", - " if not written_files:\n", - " return {**state, \"current_files\": \"No files written.\"}\n", - " return {\n", - " **state,\n", - " \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n", - " + \"\\n\".join([f\" - {f}\" for f in written_files]),\n", - " }\n", - "\n", - "\n", - "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", - "\n", - "doc_writer_agent = create_agent(\n", - " llm,\n", - " [write_document, edit_document, read_document],\n", - " \"You are an expert writing a research document.\\n\"\n", - " # The {current_files} value is populated automatically by the graph state\n", - " \"Below are files currently in your directory:\\n{current_files}\",\n", - ")\n", - "# Injects current directory working state before each call\n", - "context_aware_doc_writer_agent = prelude | doc_writer_agent\n", - "doc_writing_node = functools.partial(\n", - " agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n", - ")\n", - "\n", - "note_taking_agent = create_agent(\n", - " llm,\n", - " [create_outline, read_document],\n", - " \"You are an expert senior researcher tasked with writing a paper outline and\"\n", - " \" taking notes to craft a perfect paper.{current_files}\",\n", - ")\n", - "context_aware_note_taking_agent = prelude | note_taking_agent\n", - "note_taking_node = functools.partial(\n", - " agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n", - ")\n", - "\n", - "chart_generating_agent = create_agent(\n", - " llm,\n", - " [read_document, python_repl],\n", - " \"You are a data viz expert tasked with generating charts for a research project.\"\n", - " \"{current_files}\",\n", - ")\n", - "context_aware_chart_generating_agent = prelude | chart_generating_agent\n", - "chart_generating_node = functools.partial(\n", - " agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n", - ")\n", - "\n", - "doc_writing_supervisor = create_team_supervisor(\n", - " llm,\n", - " \"You are a supervisor tasked with managing a conversation between the\"\n", - " \" following workers: {team_members}. Given the following user request,\"\n", - " \" respond with the worker to act next. Each worker will perform a\"\n", - " \" task and respond with their results and status. When finished,\"\n", - " \" respond with FINISH.\",\n", - " [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n", - ")" - ] + "source": ["import operator\nfrom pathlib import Path\n\n\n# Document writing team graph state\nclass DocWritingState(TypedDict):\n # This tracks the team's conversation internally\n messages: Annotated[List[BaseMessage], operator.add]\n # This provides each worker with context on the others' skill sets\n team_members: str\n # This is how the supervisor tells langgraph who to work next\n next: str\n # This tracks the shared directory state\n current_files: str\n\n\n# This will be run before each worker agent begins work\n# It makes it so they are more aware of the current state\n# of the working directory.\ndef prelude(state):\n written_files = []\n if not WORKING_DIRECTORY.exists():\n WORKING_DIRECTORY.mkdir()\n try:\n written_files = [\n f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n ]\n except Exception:\n pass\n if not written_files:\n return {**state, \"current_files\": \"No files written.\"}\n return {\n **state,\n \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n + \"\\n\".join([f\" - {f}\" for f in written_files]),\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\ndoc_writer_agent = create_agent(\n llm,\n [write_document, edit_document, read_document],\n \"You are an expert writing a research document.\\n\"\n # The {current_files} value is populated automatically by the graph state\n \"Below are files currently in your directory:\\n{current_files}\",\n)\n# Injects current directory working state before each call\ncontext_aware_doc_writer_agent = prelude | doc_writer_agent\ndoc_writing_node = functools.partial(\n agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n)\n\nnote_taking_agent = create_agent(\n llm,\n [create_outline, read_document],\n \"You are an expert senior researcher tasked with writing a paper outline and\"\n \" taking notes to craft a perfect paper.{current_files}\",\n)\ncontext_aware_note_taking_agent = prelude | note_taking_agent\nnote_taking_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n)\n\nchart_generating_agent = create_agent(\n llm,\n [read_document, python_repl],\n \"You are a data viz expert tasked with generating charts for a research project.\"\n \"{current_files}\",\n)\ncontext_aware_chart_generating_agent = prelude | chart_generating_agent\nchart_generating_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n)\n\ndoc_writing_supervisor = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n)"] }, { "cell_type": "markdown", @@ -658,53 +275,7 @@ } }, "outputs": [], - "source": [ - "# Create the graph here:\n", - "# Note that we have unrolled the loop for the sake of this doc\n", - "authoring_graph = StateGraph(DocWritingState)\n", - "authoring_graph.add_node(\"DocWriter\", doc_writing_node)\n", - "authoring_graph.add_node(\"NoteTaker\", note_taking_node)\n", - "authoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\n", - "authoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n", - "\n", - "# Add the edges that always occur\n", - "authoring_graph.add_edge(\"DocWriter\", \"supervisor\")\n", - "authoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\n", - "authoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n", - "\n", - "# Add the edges where routing applies\n", - "authoring_graph.add_conditional_edges(\n", - " \"supervisor\",\n", - " lambda x: x[\"next\"],\n", - " {\n", - " \"DocWriter\": \"DocWriter\",\n", - " \"NoteTaker\": \"NoteTaker\",\n", - " \"ChartGenerator\": \"ChartGenerator\",\n", - " \"FINISH\": END,\n", - " },\n", - ")\n", - "\n", - "authoring_graph.set_entry_point(\"supervisor\")\n", - "chain = authoring_graph.compile()\n", - "\n", - "\n", - "# The following functions interoperate between the top level graph state\n", - "# and the state of the research sub-graph\n", - "# this makes it so that the states of each graph don't get intermixed\n", - "def enter_chain(message: str, members: List[str]):\n", - " results = {\n", - " \"messages\": [HumanMessage(content=message)],\n", - " \"team_members\": \", \".join(members),\n", - " }\n", - " return results\n", - "\n", - "\n", - "# We reuse the enter/exit functions to wrap the graph\n", - "authoring_chain = (\n", - " functools.partial(enter_chain, members=authoring_graph.nodes)\n", - " | authoring_graph.compile()\n", - ")" - ] + "source": ["# Create the graph here:\n# Note that we have unrolled the loop for the sake of this doc\nauthoring_graph = StateGraph(DocWritingState)\nauthoring_graph.add_node(\"DocWriter\", doc_writing_node)\nauthoring_graph.add_node(\"NoteTaker\", note_taking_node)\nauthoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\nauthoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n\n# Add the edges that always occur\nauthoring_graph.add_edge(\"DocWriter\", \"supervisor\")\nauthoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\nauthoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n\n# Add the edges where routing applies\nauthoring_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"DocWriter\": \"DocWriter\",\n \"NoteTaker\": \"NoteTaker\",\n \"ChartGenerator\": \"ChartGenerator\",\n \"FINISH\": END,\n },\n)\n\nauthoring_graph.add_edge(START, \"supervisor\")\nchain = authoring_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str, members: List[str]):\n results = {\n \"messages\": [HumanMessage(content=message)],\n \"team_members\": \", \".join(members),\n }\n return results\n\n\n# We reuse the enter/exit functions to wrap the graph\nauthoring_chain = (\n functools.partial(enter_chain, members=authoring_graph.nodes)\n | authoring_graph.compile()\n)"] }, { "cell_type": "code", @@ -728,11 +299,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(chain.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph().draw_mermaid_png()))"] }, { "cell_type": "code", @@ -745,15 +312,7 @@ } }, "outputs": [], - "source": [ - "for s in authoring_chain.stream(\n", - " \"Write an outline for poem and then write the poem to disk.\",\n", - " {\"recursion_limit\": 100},\n", - "):\n", - " if \"__end__\" not in s:\n", - " print(s)\n", - " print(\"---\")" - ] + "source": ["for s in authoring_chain.stream(\n \"Write an outline for poem and then write the poem to disk.\",\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] }, { "cell_type": "markdown", @@ -778,22 +337,7 @@ } }, "outputs": [], - "source": [ - "from langchain_core.messages import BaseMessage\n", - "from langchain_openai.chat_models import ChatOpenAI\n", - "\n", - "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", - "\n", - "supervisor_node = create_team_supervisor(\n", - " llm,\n", - " \"You are a supervisor tasked with managing a conversation between the\"\n", - " \" following teams: {team_members}. Given the following user request,\"\n", - " \" respond with the worker to act next. Each worker will perform a\"\n", - " \" task and respond with their results and status. When finished,\"\n", - " \" respond with FINISH.\",\n", - " [\"ResearchTeam\", \"PaperWritingTeam\"],\n", - ")" - ] + "source": ["from langchain_core.messages import BaseMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_node = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following teams: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"ResearchTeam\", \"PaperWritingTeam\"],\n)"] }, { "cell_type": "code", @@ -806,46 +350,7 @@ } }, "outputs": [], - "source": [ - "# Top-level graph state\n", - "class State(TypedDict):\n", - " messages: Annotated[List[BaseMessage], operator.add]\n", - " next: str\n", - "\n", - "\n", - "def get_last_message(state: State) -> str:\n", - " return state[\"messages\"][-1].content\n", - "\n", - "\n", - "def join_graph(response: dict):\n", - " return {\"messages\": [response[\"messages\"][-1]]}\n", - "\n", - "\n", - "# Define the graph.\n", - "super_graph = StateGraph(State)\n", - "# First add the nodes, which will do the work\n", - "super_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\n", - "super_graph.add_node(\n", - " \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n", - ")\n", - "super_graph.add_node(\"supervisor\", supervisor_node)\n", - "\n", - "# Define the graph connections, which controls how the logic\n", - "# propagates through the program\n", - "super_graph.add_edge(\"ResearchTeam\", \"supervisor\")\n", - "super_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\n", - "super_graph.add_conditional_edges(\n", - " \"supervisor\",\n", - " lambda x: x[\"next\"],\n", - " {\n", - " \"PaperWritingTeam\": \"PaperWritingTeam\",\n", - " \"ResearchTeam\": \"ResearchTeam\",\n", - " \"FINISH\": END,\n", - " },\n", - ")\n", - "super_graph.set_entry_point(\"supervisor\")\n", - "super_graph = super_graph.compile()" - ] + "source": ["# Top-level graph state\nclass State(TypedDict):\n messages: Annotated[List[BaseMessage], operator.add]\n next: str\n\n\ndef get_last_message(state: State) -> str:\n return state[\"messages\"][-1].content\n\n\ndef join_graph(response: dict):\n return {\"messages\": [response[\"messages\"][-1]]}\n\n\n# Define the graph.\nsuper_graph = StateGraph(State)\n# First add the nodes, which will do the work\nsuper_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\nsuper_graph.add_node(\n \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n)\nsuper_graph.add_node(\"supervisor\", supervisor_node)\n\n# Define the graph connections, which controls how the logic\n# propagates through the program\nsuper_graph.add_edge(\"ResearchTeam\", \"supervisor\")\nsuper_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\nsuper_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"PaperWritingTeam\": \"PaperWritingTeam\",\n \"ResearchTeam\": \"ResearchTeam\",\n \"FINISH\": END,\n },\n)\nsuper_graph.add_edge(START, \"supervisor\")\nsuper_graph = super_graph.compile()"] }, { "cell_type": "code", @@ -869,11 +374,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(super_graph.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(super_graph.get_graph().draw_mermaid_png()))"] }, { "cell_type": "code", @@ -886,21 +387,7 @@ } }, "outputs": [], - "source": [ - "for s in super_graph.stream(\n", - " {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n", - " )\n", - " ],\n", - " },\n", - " {\"recursion_limit\": 150},\n", - "):\n", - " if \"__end__\" not in s:\n", - " print(s)\n", - " print(\"---\")" - ] + "source": ["for s in super_graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n )\n ],\n },\n {\"recursion_limit\": 150},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] } ], "metadata": { diff --git a/examples/multi_agent/multi-agent-collaboration.ipynb b/examples/multi_agent/multi-agent-collaboration.ipynb index 04603d01c..5c2c89284 100644 --- a/examples/multi_agent/multi-agent-collaboration.ipynb +++ b/examples/multi_agent/multi-agent-collaboration.ipynb @@ -26,10 +26,7 @@ "id": "0d7b6dcc-c985-46e2-8457-7e6b0298b950", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core" - ] + "source": ["%%capture --no-stderr\n%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core"] }, { "cell_type": "code", @@ -37,24 +34,7 @@ "id": "743c19df-6da9-4d1e-b2d2-ea40080b9fdc", "metadata": {}, "outputs": [], - "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", - "_set_if_undefined(\"TAVILY_API_KEY\")\n", - "\n", - "# Optional, add tracing in LangSmith\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" - ] + "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_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] }, { "cell_type": "markdown", @@ -74,38 +54,7 @@ "id": "4325a10e-38dc-4a98-9004-e1525eaba377", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import (\n", - " BaseMessage,\n", - " HumanMessage,\n", - " ToolMessage,\n", - ")\n", - "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "\n", - "def create_agent(llm, tools, system_message: str):\n", - " \"\"\"Create an agent.\"\"\"\n", - " prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a helpful AI assistant, collaborating with other assistants.\"\n", - " \" Use the provided tools to progress towards answering the question.\"\n", - " \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n", - " \" will help where you left off. Execute what you can to make progress.\"\n", - " \" If you or any of the other assistants have the final answer or deliverable,\"\n", - " \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n", - " \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n", - " ),\n", - " MessagesPlaceholder(variable_name=\"messages\"),\n", - " ]\n", - " )\n", - " prompt = prompt.partial(system_message=system_message)\n", - " prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n", - " return prompt | llm.bind_tools(tools)" - ] + "source": ["from langchain_core.messages import (\n BaseMessage,\n HumanMessage,\n ToolMessage,\n)\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(llm, tools, system_message: str):\n \"\"\"Create an agent.\"\"\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful AI assistant, collaborating with other assistants.\"\n \" Use the provided tools to progress towards answering the question.\"\n \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n \" will help where you left off. Execute what you can to make progress.\"\n \" If you or any of the other assistants have the final answer or deliverable,\"\n \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n )\n prompt = prompt.partial(system_message=system_message)\n prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n return prompt | llm.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -123,35 +72,7 @@ "id": "ca076f3b-a729-4ca9-8f91-05c2ba58d610", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.tools import tool\n", - "from langchain_experimental.utilities import PythonREPL\n", - "\n", - "tavily_tool = TavilySearchResults(max_results=5)\n", - "\n", - "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", - "\n", - "repl = PythonREPL()\n", - "\n", - "\n", - "@tool\n", - "def python_repl(\n", - " code: Annotated[str, \"The python code to execute to generate your chart.\"],\n", - "):\n", - " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", - " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", - " try:\n", - " result = repl.run(code)\n", - " except BaseException as e:\n", - " return f\"Failed to execute. Error: {repr(e)}\"\n", - " result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n", - " return (\n", - " result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n", - " )" - ] + "source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\nfrom langchain_experimental.utilities import PythonREPL\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n return (\n result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n )"] }, { "cell_type": "markdown", @@ -179,19 +100,7 @@ "id": "290c91d4-f6f4-443c-8181-233d39102974", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "\n", - "# This defines the object that is passed between each node\n", - "# in the graph. We will create different nodes for each agent and tool\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]\n", - " sender: str" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_openai import ChatOpenAI\n\n\n# This defines the object that is passed between each node\n# in the graph. We will create different nodes for each agent and tool\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n sender: str"] }, { "cell_type": "markdown", @@ -209,46 +118,7 @@ "id": "71b790ca-9cef-4b22-b469-4b1d5d8424d6", "metadata": {}, "outputs": [], - "source": [ - "import functools\n", - "\n", - "from langchain_core.messages import AIMessage\n", - "\n", - "\n", - "# Helper function to create a node for a given agent\n", - "def agent_node(state, agent, name):\n", - " result = agent.invoke(state)\n", - " # We convert the agent output into a format that is suitable to append to the global state\n", - " if isinstance(result, ToolMessage):\n", - " pass\n", - " else:\n", - " result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n", - " return {\n", - " \"messages\": [result],\n", - " # Since we have a strict workflow, we can\n", - " # track the sender so we know who to pass to next.\n", - " \"sender\": name,\n", - " }\n", - "\n", - "\n", - "llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n", - "\n", - "# Research agent and node\n", - "research_agent = create_agent(\n", - " llm,\n", - " [tavily_tool],\n", - " system_message=\"You should provide accurate data for the chart_generator to use.\",\n", - ")\n", - "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n", - "\n", - "# chart_generator\n", - "chart_agent = create_agent(\n", - " llm,\n", - " [python_repl],\n", - " system_message=\"Any charts you display will be visible by the user.\",\n", - ")\n", - "chart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")" - ] + "source": ["import functools\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to create a node for a given agent\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n # We convert the agent output into a format that is suitable to append to the global state\n if isinstance(result, ToolMessage):\n pass\n else:\n result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n return {\n \"messages\": [result],\n # Since we have a strict workflow, we can\n # track the sender so we know who to pass to next.\n \"sender\": name,\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\n# Research agent and node\nresearch_agent = create_agent(\n llm,\n [tavily_tool],\n system_message=\"You should provide accurate data for the chart_generator to use.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# chart_generator\nchart_agent = create_agent(\n llm,\n [python_repl],\n system_message=\"Any charts you display will be visible by the user.\",\n)\nchart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")"] }, { "cell_type": "markdown", @@ -266,12 +136,7 @@ "id": "d9a79c76-5c7c-42f6-91cf-635bc8305804", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tools = [tavily_tool, python_repl]\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntools = [tavily_tool, python_repl]\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -289,23 +154,7 @@ "id": "4f4b4d37-e8a3-4abb-8d42-eaea26016f35", "metadata": {}, "outputs": [], - "source": [ - "# Either agent can decide to end\n", - "from typing import Literal\n", - "\n", - "\n", - "def router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n", - " # This is the router\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " if last_message.tool_calls:\n", - " # The previous agent is invoking a tool\n", - " return \"call_tool\"\n", - " if \"FINAL ANSWER\" in last_message.content:\n", - " # Any agent decided the work is done\n", - " return \"__end__\"\n", - " return \"continue\"" - ] + "source": ["# Either agent can decide to end\nfrom typing import Literal\n\n\ndef router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n # This is the router\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message.tool_calls:\n # The previous agent is invoking a tool\n return \"call_tool\"\n if \"FINAL ANSWER\" in last_message.content:\n # Any agent decided the work is done\n return \"__end__\"\n return \"continue\""] }, { "cell_type": "markdown", @@ -323,39 +172,7 @@ "id": "4dce3901-6ad5-4df5-8528-6e865cf96cb0", "metadata": {}, "outputs": [], - "source": [ - "workflow = StateGraph(AgentState)\n", - "\n", - "workflow.add_node(\"Researcher\", research_node)\n", - "workflow.add_node(\"chart_generator\", chart_node)\n", - "workflow.add_node(\"call_tool\", tool_node)\n", - "\n", - "workflow.add_conditional_edges(\n", - " \"Researcher\",\n", - " router,\n", - " {\"continue\": \"chart_generator\", \"call_tool\": \"call_tool\", \"__end__\": END},\n", - ")\n", - "workflow.add_conditional_edges(\n", - " \"chart_generator\",\n", - " router,\n", - " {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"__end__\": END},\n", - ")\n", - "\n", - "workflow.add_conditional_edges(\n", - " \"call_tool\",\n", - " # Each agent node updates the 'sender' field\n", - " # the tool calling node does not, meaning\n", - " # this edge will route back to the original agent\n", - " # who invoked the tool\n", - " lambda x: x[\"sender\"],\n", - " {\n", - " \"Researcher\": \"Researcher\",\n", - " \"chart_generator\": \"chart_generator\",\n", - " },\n", - ")\n", - "workflow.set_entry_point(\"Researcher\")\n", - "graph = workflow.compile()" - ] + "source": ["workflow = StateGraph(AgentState)\n\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"chart_generator\", chart_node)\nworkflow.add_node(\"call_tool\", tool_node)\n\nworkflow.add_conditional_edges(\n \"Researcher\",\n router,\n {\"continue\": \"chart_generator\", \"call_tool\": \"call_tool\", \"__end__\": END},\n)\nworkflow.add_conditional_edges(\n \"chart_generator\",\n router,\n {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"__end__\": END},\n)\n\nworkflow.add_conditional_edges(\n \"call_tool\",\n # Each agent node updates the 'sender' field\n # the tool calling node does not, meaning\n # this edge will route back to the original agent\n # who invoked the tool\n lambda x: x[\"sender\"],\n {\n \"Researcher\": \"Researcher\",\n \"chart_generator\": \"chart_generator\",\n },\n)\nworkflow.add_edge(START, \"Researcher\")\ngraph = workflow.compile()"] }, { "cell_type": "code", @@ -374,15 +191,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -481,24 +290,7 @@ ] } ], - "source": [ - "events = graph.stream(\n", - " {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"Fetch the UK's GDP over the past 5 years,\"\n", - " \" then draw a line graph of it.\"\n", - " \" Once you code it up, finish.\"\n", - " )\n", - " ],\n", - " },\n", - " # Maximum number of steps to take in the graph\n", - " {\"recursion_limit\": 150},\n", - ")\n", - "for s in events:\n", - " print(s)\n", - " print(\"----\")" - ] + "source": ["events = graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Fetch the UK's GDP over the past 5 years,\"\n \" then draw a line graph of it.\"\n \" Once you code it up, finish.\"\n )\n ],\n },\n # Maximum number of steps to take in the graph\n {\"recursion_limit\": 150},\n)\nfor s in events:\n print(s)\n print(\"----\")"] }, { "cell_type": "code", @@ -506,7 +298,7 @@ "id": "010fc36e-4116-4758-bcac-b02c7dcd405d", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/pass-run-time-values-to-tools.ipynb b/examples/pass-run-time-values-to-tools.ipynb index 8a39250c3..f8af76496 100644 --- a/examples/pass-run-time-values-to-tools.ipynb +++ b/examples/pass-run-time-values-to-tools.ipynb @@ -35,10 +35,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai"] }, { "cell_type": "markdown", @@ -54,13 +51,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "if \"OPENAI_API_KEY\" not in os.environ:\n", - " os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" - ] + "source": ["import getpass\nimport os\n\nif \"OPENAI_API_KEY\" not in os.environ:\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"] }, { "cell_type": "markdown", @@ -76,12 +67,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "\n", - "if \"LANGCHAIN_API_KEY\" not in os.environ:\n", - " os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n\nif \"LANGCHAIN_API_KEY\" not in os.environ:\n os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] }, { "cell_type": "markdown", @@ -102,36 +88,7 @@ "id": "1d36e782-80f4-4334-b7d7-ee4c79864480", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from langchain_core.tools import BaseTool, tool\n", - "\n", - "# A global dict that the tools will be updating in this example.\n", - "user_to_pets = {}\n", - "\n", - "\n", - "def generate_tools_for_user(user_id: str) -> List[BaseTool]:\n", - " \"\"\"Generate a set of tools that have a user id associated with them.\"\"\"\n", - "\n", - " @tool\n", - " def update_favorite_pets(pets: List[str]) -> None:\n", - " \"\"\"Add the list of favorite pets.\"\"\"\n", - " user_to_pets[user_id] = pets\n", - "\n", - " @tool\n", - " def delete_favorite_pets() -> None:\n", - " \"\"\"Delete the list of favorite pets.\"\"\"\n", - " if user_id in user_to_pets:\n", - " del user_to_pets[user_id]\n", - "\n", - " @tool\n", - " def list_favorite_pets() -> None:\n", - " \"\"\"List favorite pets if any.\"\"\"\n", - " return user_to_pets.get(user_id, [])\n", - "\n", - " return [update_favorite_pets, delete_favorite_pets, list_favorite_pets]" - ] + "source": ["from typing import List\n\nfrom langchain_core.tools import BaseTool, tool\n\n# A global dict that the tools will be updating in this example.\nuser_to_pets = {}\n\n\ndef generate_tools_for_user(user_id: str) -> List[BaseTool]:\n \"\"\"Generate a set of tools that have a user id associated with them.\"\"\"\n\n @tool\n def update_favorite_pets(pets: List[str]) -> None:\n \"\"\"Add the list of favorite pets.\"\"\"\n user_to_pets[user_id] = pets\n\n @tool\n def delete_favorite_pets() -> None:\n \"\"\"Delete the list of favorite pets.\"\"\"\n if user_id in user_to_pets:\n del user_to_pets[user_id]\n\n @tool\n def list_favorite_pets() -> None:\n \"\"\"List favorite pets if any.\"\"\"\n return user_to_pets.get(user_id, [])\n\n return [update_favorite_pets, delete_favorite_pets, list_favorite_pets]"] }, { "cell_type": "markdown", @@ -155,13 +112,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -187,16 +138,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -231,69 +173,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolExecutor, ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state, config):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state, config):\n", - " messages = state[\"messages\"]\n", - " tools = generate_tools_for_user(config[\"user_id\"])\n", - " model_with_tools = model.bind_tools(tools)\n", - " response = model_with_tools.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state, config):\n", - " messages = state[\"messages\"]\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation for each tool call\n", - " tool_invocations = []\n", - " for tool_call in last_message.tool_calls:\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " tool_invocations.append(action)\n", - "\n", - " # We call the tool_executor and get back a response\n", - " # We can now wrap these tools in a simple ToolExecutor.\n", - " # This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n", - " # A ToolInvocation is any class with `tool` and `tool_input` attribute.\n", - " tools = generate_tools_for_user(config[\"user_id\"])\n", - " tool_executor = ToolExecutor(tools)\n", - " responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n", - " # We use the response to create tool messages\n", - " tool_messages = [\n", - " ToolMessage(\n", - " content=str(response),\n", - " name=tc[\"name\"],\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc, response in zip(last_message.tool_calls, responses)\n", - " ]\n", - "\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolExecutor, ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state, config):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state, config):\n messages = state[\"messages\"]\n tools = generate_tools_for_user(config[\"user_id\"])\n model_with_tools = model.bind_tools(tools)\n response = model_with_tools.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state, config):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n # We call the tool_executor and get back a response\n # We can now wrap these tools in a simple ToolExecutor.\n # This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n # A ToolInvocation is any class with `tool` and `tool_input` attribute.\n tools = generate_tools_for_user(config[\"user_id\"])\n tool_executor = ToolExecutor(tools)\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] }, { "cell_type": "markdown", @@ -311,50 +191,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -373,15 +210,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -427,24 +256,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "user_to_pets.clear() # Clear the state\n", - "\n", - "print(f\"User information prior to run: {user_to_pets}\")\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"my favorite pets are cats and dogs\")]}\n", - "for output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")\n", - "\n", - "print(f\"User information prior to run: {user_to_pets}\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nuser_to_pets.clear() # Clear the state\n\nprint(f\"User information prior to run: {user_to_pets}\")\n\ninputs = {\"messages\": [HumanMessage(content=\"my favorite pets are cats and dogs\")]}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\nprint(f\"User information prior to run: {user_to_pets}\")"] }, { "cell_type": "code", @@ -479,22 +291,7 @@ ] } ], - "source": [ - "print(f\"User information prior to run: {user_to_pets}\")\n", - "\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what are my favorite pets?\")]}\n", - "for output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")\n", - "\n", - "\n", - "print(f\"User information prior to run: {user_to_pets}\")" - ] + "source": ["print(f\"User information prior to run: {user_to_pets}\")\n\n\ninputs = {\"messages\": [HumanMessage(content=\"what are my favorite pets?\")]}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\n\nprint(f\"User information prior to run: {user_to_pets}\")"] }, { "cell_type": "code", @@ -529,26 +326,7 @@ ] } ], - "source": [ - "print(f\"User information prior to run: {user_to_pets}\")\n", - "\n", - "\n", - "inputs = {\n", - " \"messages\": [\n", - " HumanMessage(content=\"please forget what i told you about my favorite animals\")\n", - " ]\n", - "}\n", - "for output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n", - " # stream() yields dictionaries with output keyed by node name\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")\n", - "\n", - "\n", - "print(f\"User information prior to run: {user_to_pets}\")" - ] + "source": ["print(f\"User information prior to run: {user_to_pets}\")\n\n\ninputs = {\n \"messages\": [\n HumanMessage(content=\"please forget what i told you about my favorite animals\")\n ]\n}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\n\nprint(f\"User information prior to run: {user_to_pets}\")"] } ], "metadata": { diff --git a/examples/persistence.ipynb b/examples/persistence.ipynb index 4977720bb..7fd7b4e65 100644 --- a/examples/persistence.ipynb +++ b/examples/persistence.ipynb @@ -44,7 +44,7 @@ "id": "0c3fde0a", "metadata": {}, "outputs": [], - "source": [] + "source": [""] }, { "cell_type": "markdown", @@ -62,10 +62,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"] }, { "cell_type": "markdown", @@ -81,18 +78,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -108,10 +94,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -129,22 +112,7 @@ "id": "14619607", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# Add messages essentially does this with more\n", - "# robust handling\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -164,19 +132,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " return [\"The answer to your question lies within.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -193,11 +149,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -227,13 +179,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "# We will set streaming=True so that we can stream tokens\n", - "# See the streaming section for more information on this.\n", - "model = ChatOpenAI(temperature=0, streaming=True)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] }, { "cell_type": "markdown", @@ -251,9 +197,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "bound_model = model.bind_tools(tools)" - ] + "source": ["bound_model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -288,27 +232,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "# Define the function that determines whether to continue or not\n", - "from typing import Literal\n", - "\n", - "\n", - "def should_continue(state: State) -> Literal[\"action\", \"__end__\"]:\n", - " \"\"\"Return the next node to execute.\"\"\"\n", - " last_message = state[\"messages\"][-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"__end__\"\n", - " # Otherwise if there is, we continue\n", - " return \"action\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state: State):\n", - " response = model.invoke(state[\"messages\"])\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": response}" - ] + "source": ["# Define the function that determines whether to continue or not\nfrom typing import Literal\n\n\ndef should_continue(state: State) -> Literal[\"action\", \"__end__\"]:\n \"\"\"Return the next node to execute.\"\"\"\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"__end__\"\n # Otherwise if there is, we continue\n return \"action\"\n\n\n# Define the function that calls the model\ndef call_model(state: State):\n response = model.invoke(state[\"messages\"])\n # We return a list, because this will get added to the existing list\n return {\"messages\": response}"] }, { "cell_type": "markdown", @@ -324,33 +248,7 @@ "id": "812b4e70-4956-4415-8880-db48b3dcbad2", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")" - ] + "source": ["from langgraph.graph import StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"] }, { "cell_type": "markdown", @@ -368,11 +266,7 @@ "id": "6845ed6a-d155-4105-9160-28849877248b", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "code", @@ -380,12 +274,7 @@ "id": "79d29875-8aa8-434c-9f20-1c58346a6249", "metadata": {}, "outputs": [], - "source": [ - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile(checkpointer=memory)" - ] + "source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"] }, { "cell_type": "code", @@ -404,15 +293,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph().draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -443,14 +324,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "input_message = HumanMessage(content=\"hi! I'm bob\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -471,11 +345,7 @@ ] } ], - "source": [ - "input_message = HumanMessage(content=\"what is my name?\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["input_message = HumanMessage(content=\"what is my name?\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -504,15 +374,7 @@ ] } ], - "source": [ - "input_message = HumanMessage(content=\"what is my name?\")\n", - "for event in app.stream(\n", - " {\"messages\": [input_message]},\n", - " {\"configurable\": {\"thread_id\": \"3\"}},\n", - " stream_mode=\"values\",\n", - "):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["input_message = HumanMessage(content=\"what is my name?\")\nfor event in app.stream(\n {\"messages\": [input_message]},\n {\"configurable\": {\"thread_id\": \"3\"}},\n stream_mode=\"values\",\n):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -541,15 +403,7 @@ ] } ], - "source": [ - "input_message = HumanMessage(content=\"You forgot??\")\n", - "for event in app.stream(\n", - " {\"messages\": [input_message]},\n", - " {\"configurable\": {\"thread_id\": \"2\"}},\n", - " stream_mode=\"values\",\n", - "):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["input_message = HumanMessage(content=\"You forgot??\")\nfor event in app.stream(\n {\"messages\": [input_message]},\n {\"configurable\": {\"thread_id\": \"2\"}},\n stream_mode=\"values\",\n):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -557,7 +411,7 @@ "id": "eb20430f", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/plan-and-execute/plan-and-execute.ipynb b/examples/plan-and-execute/plan-and-execute.ipynb index 5f044c5eb..95eb7d384 100644 --- a/examples/plan-and-execute/plan-and-execute.ipynb +++ b/examples/plan-and-execute/plan-and-execute.ipynb @@ -45,10 +45,7 @@ "id": "b451b58a-89bd-424f-8c06-0d9fe325e01b", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python"] }, { "cell_type": "markdown", @@ -64,19 +61,7 @@ "id": "ce438281-08d5-4804-afe7-e4089f7b016b", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")\n", - "_set_env(\"TAVILY_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")"] }, { "cell_type": "markdown", @@ -92,11 +77,7 @@ "id": "01f460d1-f26f-47d1-ae76-de74d5d851de", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"Plan-and-execute\"" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Plan-and-execute\""] }, { "cell_type": "markdown", @@ -114,11 +95,7 @@ "id": "25b9ec62-0675-4715-811c-9b32c635b22f", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "tools = [TavilySearchResults(max_results=3)]" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=3)]"] }, { "cell_type": "markdown", @@ -151,20 +128,7 @@ ] } ], - "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "from langgraph.prebuilt import create_react_agent\n", - "\n", - "# Get the prompt to use - you can modify this!\n", - "prompt = hub.pull(\"wfh/react-agent-executor\")\n", - "prompt.pretty_print()\n", - "\n", - "# Choose the LLM that will drive the agent\n", - "llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", - "agent_executor = create_react_agent(llm, tools, messages_modifier=prompt)" - ] + "source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import create_react_agent\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"wfh/react-agent-executor\")\nprompt.pretty_print()\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\nagent_executor = create_react_agent(llm, tools, messages_modifier=prompt)"] }, { "cell_type": "code", @@ -186,9 +150,7 @@ "output_type": "execute_result" } ], - "source": [ - "agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})" - ] + "source": ["agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})"] }, { "cell_type": "markdown", @@ -212,17 +174,7 @@ "id": "8eeeaeea-8f10-4fbe-8e24-4e1a2381a009", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, List, Tuple, TypedDict\n", - "\n", - "\n", - "class PlanExecute(TypedDict):\n", - " input: str\n", - " plan: List[str]\n", - " past_steps: Annotated[List[Tuple], operator.add]\n", - " response: str" - ] + "source": ["import operator\nfrom typing import Annotated, List, Tuple, TypedDict\n\n\nclass PlanExecute(TypedDict):\n input: str\n plan: List[str]\n past_steps: Annotated[List[Tuple], operator.add]\n response: str"] }, { "cell_type": "markdown", @@ -240,17 +192,7 @@ "id": "4a88626d-6dfd-4488-87f0-a9a0dd6da44c", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class Plan(BaseModel):\n", - " \"\"\"Plan to follow in future\"\"\"\n", - "\n", - " steps: List[str] = Field(\n", - " description=\"different steps to follow, should be in sorted order\"\n", - " )" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Plan(BaseModel):\n \"\"\"Plan to follow in future\"\"\"\n\n steps: List[str] = Field(\n description=\"different steps to follow, should be in sorted order\"\n )"] }, { "cell_type": "code", @@ -258,24 +200,7 @@ "id": "ec7b1867-1ea3-4df3-9a98-992a1c32ec49", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.prompts import ChatPromptTemplate\n", - "\n", - "planner_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"For the given objective, come up with a simple step by step plan. \\\n", - "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", - "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\"\"\",\n", - " ),\n", - " (\"placeholder\", \"{messages}\"),\n", - " ]\n", - ")\n", - "planner = planner_prompt | ChatOpenAI(\n", - " model=\"gpt-4o\", temperature=0\n", - ").with_structured_output(Plan)" - ] + "source": ["from langchain_core.prompts import ChatPromptTemplate\n\nplanner_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"For the given objective, come up with a simple step by step plan. \\\nThis plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\nThe result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\nplanner = planner_prompt | ChatOpenAI(\n model=\"gpt-4o\", temperature=0\n).with_structured_output(Plan)"] }, { "cell_type": "code", @@ -294,15 +219,7 @@ "output_type": "execute_result" } ], - "source": [ - "planner.invoke(\n", - " {\n", - " \"messages\": [\n", - " (\"user\", \"what is the hometown of the current Australia open winner?\")\n", - " ]\n", - " }\n", - ")" - ] + "source": ["planner.invoke(\n {\n \"messages\": [\n (\"user\", \"what is the hometown of the current Australia open winner?\")\n ]\n }\n)"] }, { "cell_type": "markdown", @@ -320,47 +237,7 @@ "id": "ec2d12cc-016a-44d1-aa08-4c5ce1e8fe2a", "metadata": {}, "outputs": [], - "source": [ - "from typing import Union\n", - "\n", - "\n", - "class Response(BaseModel):\n", - " \"\"\"Response to user.\"\"\"\n", - "\n", - " response: str\n", - "\n", - "\n", - "class Act(BaseModel):\n", - " \"\"\"Action to perform.\"\"\"\n", - "\n", - " action: Union[Response, Plan] = Field(\n", - " description=\"Action to perform. If you want to respond to user, use Response. \"\n", - " \"If you need to further use tools to get the answer, use Plan.\"\n", - " )\n", - "\n", - "\n", - "replanner_prompt = ChatPromptTemplate.from_template(\n", - " \"\"\"For the given objective, come up with a simple step by step plan. \\\n", - "This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n", - "The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n", - "\n", - "Your objective was this:\n", - "{input}\n", - "\n", - "Your original plan was this:\n", - "{plan}\n", - "\n", - "You have currently done the follow steps:\n", - "{past_steps}\n", - "\n", - "Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.\"\"\"\n", - ")\n", - "\n", - "\n", - "replanner = replanner_prompt | ChatOpenAI(\n", - " model=\"gpt-4o\", temperature=0\n", - ").with_structured_output(Act)" - ] + "source": ["from typing import Union\n\n\nclass Response(BaseModel):\n \"\"\"Response to user.\"\"\"\n\n response: str\n\n\nclass Act(BaseModel):\n \"\"\"Action to perform.\"\"\"\n\n action: Union[Response, Plan] = Field(\n description=\"Action to perform. If you want to respond to user, use Response. \"\n \"If you need to further use tools to get the answer, use Plan.\"\n )\n\n\nreplanner_prompt = ChatPromptTemplate.from_template(\n \"\"\"For the given objective, come up with a simple step by step plan. \\\nThis plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\nThe result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n\nYour objective was this:\n{input}\n\nYour original plan was this:\n{plan}\n\nYou have currently done the follow steps:\n{past_steps}\n\nUpdate your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.\"\"\"\n)\n\n\nreplanner = replanner_prompt | ChatOpenAI(\n model=\"gpt-4o\", temperature=0\n).with_structured_output(Act)"] }, { "cell_type": "markdown", @@ -378,43 +255,7 @@ "id": "6c8e0dad-bcea-4c9a-8922-0d820892e2d0", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "\n", - "async def execute_step(state: PlanExecute):\n", - " plan = state[\"plan\"]\n", - " plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n", - " task = plan[0]\n", - " task_formatted = f\"\"\"For the following plan:\n", - "{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n", - " agent_response = await agent_executor.ainvoke(\n", - " {\"messages\": [(\"user\", task_formatted)]}\n", - " )\n", - " return {\n", - " \"past_steps\": (task, agent_response[\"messages\"][-1].content),\n", - " }\n", - "\n", - "\n", - "async def plan_step(state: PlanExecute):\n", - " plan = await planner.ainvoke({\"messages\": [(\"user\", state[\"input\"])]})\n", - " return {\"plan\": plan.steps}\n", - "\n", - "\n", - "async def replan_step(state: PlanExecute):\n", - " output = await replanner.ainvoke(state)\n", - " if isinstance(output.action, Response):\n", - " return {\"response\": output.action.response}\n", - " else:\n", - " return {\"plan\": output.action.steps}\n", - "\n", - "\n", - "def should_end(state: PlanExecute) -> Literal[\"agent\", \"__end__\"]:\n", - " if \"response\" in state and state[\"response\"]:\n", - " return \"__end__\"\n", - " else:\n", - " return \"agent\"" - ] + "source": ["from typing import Literal\n\n\nasync def execute_step(state: PlanExecute):\n plan = state[\"plan\"]\n plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n task = plan[0]\n task_formatted = f\"\"\"For the following plan:\n{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n agent_response = await agent_executor.ainvoke(\n {\"messages\": [(\"user\", task_formatted)]}\n )\n return {\n \"past_steps\": (task, agent_response[\"messages\"][-1].content),\n }\n\n\nasync def plan_step(state: PlanExecute):\n plan = await planner.ainvoke({\"messages\": [(\"user\", state[\"input\"])]})\n return {\"plan\": plan.steps}\n\n\nasync def replan_step(state: PlanExecute):\n output = await replanner.ainvoke(state)\n if isinstance(output.action, Response):\n return {\"response\": output.action.response}\n else:\n return {\"plan\": output.action.steps}\n\n\ndef should_end(state: PlanExecute) -> Literal[\"agent\", \"__end__\"]:\n if \"response\" in state and state[\"response\"]:\n return \"__end__\"\n else:\n return \"agent\""] }, { "cell_type": "code", @@ -422,39 +263,7 @@ "id": "e954cea0-5ccc-46c2-a27b-f5b7185b597d", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import StateGraph\n", - "\n", - "workflow = StateGraph(PlanExecute)\n", - "\n", - "# Add the plan node\n", - "workflow.add_node(\"planner\", plan_step)\n", - "\n", - "# Add the execution step\n", - "workflow.add_node(\"agent\", execute_step)\n", - "\n", - "# Add a replan node\n", - "workflow.add_node(\"replan\", replan_step)\n", - "\n", - "workflow.set_entry_point(\"planner\")\n", - "\n", - "# From plan we go to agent\n", - "workflow.add_edge(\"planner\", \"agent\")\n", - "\n", - "# From agent, we replan\n", - "workflow.add_edge(\"agent\", \"replan\")\n", - "\n", - "workflow.add_conditional_edges(\n", - " \"replan\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_end,\n", - ")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import StateGraph, START\n\nworkflow = StateGraph(PlanExecute)\n\n# Add the plan node\nworkflow.add_node(\"planner\", plan_step)\n\n# Add the execution step\nworkflow.add_node(\"agent\", execute_step)\n\n# Add a replan node\nworkflow.add_node(\"replan\", replan_step)\n\nworkflow.add_edge(START, \"planner\")\n\n# From plan we go to agent\nworkflow.add_edge(\"planner\", \"agent\")\n\n# From agent, we replan\nworkflow.add_edge(\"agent\", \"replan\")\n\nworkflow.add_conditional_edges(\n \"replan\",\n # Next, we pass in the function that will determine which node is called next.\n should_end,\n)\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -473,11 +282,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "code", @@ -505,14 +310,7 @@ ] } ], - "source": [ - "config = {\"recursion_limit\": 50}\n", - "inputs = {\"input\": \"what is the hometown of the 2024 Australia open winner?\"}\n", - "async for event in app.astream(inputs, config=config):\n", - " for k, v in event.items():\n", - " if k != \"__end__\":\n", - " print(v)" - ] + "source": ["config = {\"recursion_limit\": 50}\ninputs = {\"input\": \"what is the hometown of the 2024 Australia open winner?\"}\nasync for event in app.astream(inputs, config=config):\n for k, v in event.items():\n if k != \"__end__\":\n print(v)"] }, { "cell_type": "markdown", @@ -530,7 +328,7 @@ "id": "ad8f7955-2cc9-4ebb-8c41-13abb3351a24", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_adaptive_rag.ipynb b/examples/rag/langgraph_adaptive_rag.ipynb index c4da78907..a7cbbff0b 100644 --- a/examples/rag/langgraph_adaptive_rag.ipynb +++ b/examples/rag/langgraph_adaptive_rag.ipynb @@ -46,10 +46,7 @@ "id": "53d1a740-9fea-4a6e-8f95-fb9dbf1c80a1", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python" - ] + "source": ["%%capture --no-stderr\n! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python"] }, { "cell_type": "code", @@ -57,14 +54,7 @@ "id": "222f204d-956f-4128-b597-2c698120edda", "metadata": {}, "outputs": [], - "source": [ - "### LLMs\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"\"\n", - "os.environ[\"COHERE_API_KEY\"] = \"\"\n", - "os.environ[\"TAVILY_API_KEY\"] = \"\"" - ] + "source": ["### LLMs\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\"\nos.environ[\"COHERE_API_KEY\"] = \"\"\nos.environ[\"TAVILY_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -82,12 +72,7 @@ "id": "08edba00-988a-478b-96fc-ae0199cbef49", "metadata": {}, "outputs": [], - "source": [ - "### Tracing (optional)\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["### Tracing (optional)\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -103,44 +88,7 @@ "id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c", "metadata": {}, "outputs": [], - "source": [ - "### Build Index\n", - "\n", - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "### from langchain_cohere import CohereEmbeddings\n", - "\n", - "# Set embeddings\n", - "embd = OpenAIEmbeddings()\n", - "\n", - "# Docs to index\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "# Load\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "# Split\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=500, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorstore\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=embd,\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\n### from langchain_cohere import CohereEmbeddings\n\n# Set embeddings\nembd = OpenAIEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=500, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=embd,\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -165,49 +113,7 @@ ] } ], - "source": [ - "### Router\n", - "\n", - "from typing import Literal\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "\n", - "# Data model\n", - "class RouteQuery(BaseModel):\n", - " \"\"\"Route a user query to the most relevant datasource.\"\"\"\n", - "\n", - " datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n", - " ...,\n", - " description=\"Given a user question choose to route it to web search or a vectorstore.\",\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_router = llm.with_structured_output(RouteQuery)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n", - "The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n", - "Use the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n", - "route_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"{question}\"),\n", - " ]\n", - ")\n", - "\n", - "question_router = route_prompt | structured_llm_router\n", - "print(\n", - " question_router.invoke(\n", - " {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n", - " )\n", - ")\n", - "print(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))" - ] + "source": ["### Router\n\nfrom typing import Literal\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass RouteQuery(BaseModel):\n \"\"\"Route a user query to the most relevant datasource.\"\"\"\n\n datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n ...,\n description=\"Given a user question choose to route it to web search or a vectorstore.\",\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_router = llm.with_structured_output(RouteQuery)\n\n# Prompt\nsystem = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nprint(\n question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n )\n)\nprint(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"] }, { "cell_type": "code", @@ -223,41 +129,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeDocuments(BaseModel):\n", - " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", - " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", - " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", - "grade_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", - " ]\n", - ")\n", - "\n", - "retrieval_grader = grade_prompt | structured_llm_grader\n", - "question = \"agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "code", @@ -273,31 +145,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain import hub\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", - "\n", - "\n", - "# Post-processing\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -316,36 +164,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeHallucinations(BaseModel):\n", - " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n", - " Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n", - "hallucination_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n", - " ]\n", - ")\n", - "\n", - "hallucination_grader = hallucination_prompt | structured_llm_grader\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -364,36 +183,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeAnswer(BaseModel):\n", - " \"\"\"Binary score to assess answer addresses question.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer addresses the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n", - " Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n", - "answer_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n", - " ]\n", - ")\n", - "\n", - "answer_grader = answer_prompt | structured_llm_grader\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "code", @@ -412,28 +202,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Question Re-writer\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", - " for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n", - "re_write_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\n", - " \"human\",\n", - " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n", - " ),\n", - " ]\n", - ")\n", - "\n", - "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", - "question_rewriter.invoke({\"question\": question})" - ] + "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] }, { "cell_type": "markdown", @@ -449,13 +218,7 @@ "id": "01d829bb-1074-4976-b650-ead41dcb9788", "metadata": {}, "outputs": [], - "source": [ - "### Search\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "web_search_tool = TavilySearchResults(k=3)" - ] + "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"] }, { "cell_type": "markdown", @@ -475,26 +238,7 @@ "id": "e723fcdb-06e6-402d-912e-899795b78408", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] }, { "cell_type": "markdown", @@ -510,211 +254,7 @@ "id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a", "metadata": {}, "outputs": [], - "source": [ - "from langchain.schema import Document\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.invoke(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question}\n", - "\n", - "\n", - "def transform_query(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates question key with a re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Re-write question\n", - " better_question = question_rewriter.invoke({\"question\": question})\n", - " return {\"documents\": documents, \"question\": better_question}\n", - "\n", - "\n", - "def web_search(state):\n", - " \"\"\"\n", - " Web search based on the re-phrased question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with appended web results\n", - " \"\"\"\n", - "\n", - " print(\"---WEB SEARCH---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Web search\n", - " docs = web_search_tool.invoke({\"query\": question})\n", - " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", - " web_results = Document(page_content=web_results)\n", - "\n", - " return {\"documents\": web_results, \"question\": question}\n", - "\n", - "\n", - "### Edges ###\n", - "\n", - "\n", - "def route_question(state):\n", - " \"\"\"\n", - " Route question to web search or RAG.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ROUTE QUESTION---\")\n", - " question = state[\"question\"]\n", - " source = question_router.invoke({\"question\": question})\n", - " if source.datasource == \"web_search\":\n", - " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", - " return \"web_search\"\n", - " elif source.datasource == \"vectorstore\":\n", - " print(\"---ROUTE QUESTION TO RAG---\")\n", - " return \"vectorstore\"\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " filtered_documents = state[\"documents\"]\n", - "\n", - " if not filtered_documents:\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", - " )\n", - " return \"transform_query\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score.binary_score\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"" - ] + "source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n if source.datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source.datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] }, { "cell_type": "markdown", @@ -730,50 +270,7 @@ "id": "67854e07-9293-4c3c-bf9a-bc9a605570ee", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"web_search\", web_search) # web search\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", - "\n", - "# Build graph\n", - "workflow.set_conditional_entry_point(\n", - " route_question,\n", - " {\n", - " \"web_search\": \"web_search\",\n", - " \"vectorstore\": \"retrieve\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"web_search\", \"generate\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"transform_query\": \"transform_query\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"transform_query\", \"retrieve\")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\",\n", - " \"useful\": END,\n", - " \"not useful\": \"transform_query\",\n", - " },\n", - ")\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"web_search\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"web_search\": \"web_search\",\n \"vectorstore\": \"retrieve\",\n })\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "code", @@ -805,24 +302,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\n", - " \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n", - "}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\n \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -873,20 +353,7 @@ ] } ], - "source": [ - "# Run\n", - "inputs = {\"question\": \"What are the types of agent memory?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -904,7 +371,7 @@ "id": "19ac1f6f-2d84-488f-8a0e-7ee2a46b0f71", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_adaptive_rag_cohere.ipynb b/examples/rag/langgraph_adaptive_rag_cohere.ipynb index 898727d98..93447acab 100644 --- a/examples/rag/langgraph_adaptive_rag_cohere.ipynb +++ b/examples/rag/langgraph_adaptive_rag_cohere.ipynb @@ -53,9 +53,7 @@ "id": "f6c329ba-cb85-4576-9828-4f2ac648d1a6", "metadata": {}, "outputs": [], - "source": [ - "! pip install --quiet langchain langchain_cohere langchain-openai tiktoken langchainhub chromadb langgraph" - ] + "source": ["! pip install --quiet langchain langchain_cohere langchain-openai tiktoken langchainhub chromadb langgraph"] }, { "cell_type": "code", @@ -65,12 +63,7 @@ "id": "222f204d-956f-4128-b597-2c698120edda" }, "outputs": [], - "source": [ - "### LLMs\n", - "import os\n", - "\n", - "os.environ[\"COHERE_API_KEY\"] = \"\"" - ] + "source": ["### LLMs\nimport os\n\nos.environ[\"COHERE_API_KEY\"] = \"\""] }, { "cell_type": "code", @@ -80,12 +73,7 @@ "id": "08edba00-988a-478b-96fc-ae0199cbef49" }, "outputs": [], - "source": [ - "# ### Tracing (optional)\n", - "# os.environ['LANGCHAIN_TRACING_V2'] = 'true'\n", - "# os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'\n", - "# os.environ['LANGCHAIN_API_KEY'] =''" - ] + "source": ["# ### Tracing (optional)\n# os.environ['LANGCHAIN_TRACING_V2'] = 'true'\n# os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'\n# os.environ['LANGCHAIN_API_KEY'] =''"] }, { "cell_type": "markdown", @@ -105,42 +93,7 @@ "id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c" }, "outputs": [], - "source": [ - "### Build Index\n", - "\n", - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_cohere import CohereEmbeddings\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "\n", - "# Set embeddings\n", - "embd = CohereEmbeddings()\n", - "\n", - "# Docs to index\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "# Load\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "# Split\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=512, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorstore\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " embedding=embd,\n", - ")\n", - "\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_cohere import CohereEmbeddings\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\n\n# Set embeddings\nembd = CohereEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=512, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n embedding=embd,\n)\n\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -186,59 +139,7 @@ ] } ], - "source": [ - "### Router\n", - "\n", - "from langchain_cohere import ChatCohere\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "# Data model\n", - "class web_search(BaseModel):\n", - " \"\"\"\n", - " The internet. Use web_search for questions that are related to anything else than agents, prompt engineering, and adversarial attacks.\n", - " \"\"\"\n", - "\n", - " query: str = Field(description=\"The query to use when searching the internet.\")\n", - "\n", - "\n", - "class vectorstore(BaseModel):\n", - " \"\"\"\n", - " A vectorstore containing documents related to agents, prompt engineering, and adversarial attacks. Use the vectorstore for questions on these topics.\n", - " \"\"\"\n", - "\n", - " query: str = Field(description=\"The query to use when searching the vectorstore.\")\n", - "\n", - "\n", - "# Preamble\n", - "preamble = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\n", - "The vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\n", - "Use the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n", - "\n", - "# LLM with tool use and preamble\n", - "llm = ChatCohere(model=\"command-r\", temperature=0)\n", - "structured_llm_router = llm.bind_tools(\n", - " tools=[web_search, vectorstore], preamble=preamble\n", - ")\n", - "\n", - "# Prompt\n", - "route_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"human\", \"{question}\"),\n", - " ]\n", - ")\n", - "\n", - "question_router = route_prompt | structured_llm_router\n", - "response = question_router.invoke(\n", - " {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n", - ")\n", - "print(response.response_metadata[\"tool_calls\"])\n", - "response = question_router.invoke({\"question\": \"What are the types of agent memory?\"})\n", - "print(response.response_metadata[\"tool_calls\"])\n", - "response = question_router.invoke({\"question\": \"Hi how are you?\"})\n", - "print(\"tool_calls\" in response.response_metadata)" - ] + "source": ["### Router\n\nfrom langchain_cohere import ChatCohere\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\n# Data model\nclass web_search(BaseModel):\n \"\"\"\n The internet. Use web_search for questions that are related to anything else than agents, prompt engineering, and adversarial attacks.\n \"\"\"\n\n query: str = Field(description=\"The query to use when searching the internet.\")\n\n\nclass vectorstore(BaseModel):\n \"\"\"\n A vectorstore containing documents related to agents, prompt engineering, and adversarial attacks. Use the vectorstore for questions on these topics.\n \"\"\"\n\n query: str = Field(description=\"The query to use when searching the vectorstore.\")\n\n\n# Preamble\npreamble = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n\n# LLM with tool use and preamble\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_router = llm.bind_tools(\n tools=[web_search, vectorstore], preamble=preamble\n)\n\n# Prompt\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nresponse = question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n)\nprint(response.response_metadata[\"tool_calls\"])\nresponse = question_router.invoke({\"question\": \"What are the types of agent memory?\"})\nprint(response.response_metadata[\"tool_calls\"])\nresponse = question_router.invoke({\"question\": \"Hi how are you?\"})\nprint(\"tool_calls\" in response.response_metadata)"] }, { "cell_type": "code", @@ -260,41 +161,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeDocuments(BaseModel):\n", - " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# Prompt\n", - "preamble = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n\n", - "If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", - "Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", - "\n", - "# LLM with function call\n", - "llm = ChatCohere(model=\"command-r\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeDocuments, preamble=preamble)\n", - "\n", - "grade_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", - " ]\n", - ")\n", - "\n", - "retrieval_grader = grade_prompt | structured_llm_grader\n", - "question = \"types of agent memory\"\n", - "docs = retriever.invoke(question)\n", - "doc_txt = docs[1].page_content\n", - "response = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\n", - "print(response)" - ] + "source": ["### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# Prompt\npreamble = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n\nIf the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\nGive a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments, preamble=preamble)\n\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"types of agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nresponse = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\nprint(response)"] }, { "cell_type": "markdown", @@ -326,38 +193,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain_core.messages import HumanMessage\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Preamble\n", - "preamble = \"\"\"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\"\"\"\n", - "\n", - "# LLM\n", - "llm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n", - "\n", - "\n", - "# Prompt\n", - "def prompt(x):\n", - " return ChatPromptTemplate.from_messages(\n", - " [\n", - " HumanMessage(\n", - " f\"Question: {x['question']} \\nAnswer: \",\n", - " additional_kwargs={\"documents\": x[\"documents\"]},\n", - " )\n", - " ]\n", - " )\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"documents\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Preamble\npreamble = \"\"\"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\"\"\"\n\n# LLM\nllm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n\n\n# Prompt\ndef prompt(x):\n return ChatPromptTemplate.from_messages(\n [\n HumanMessage(\n f\"Question: {x['question']} \\nAnswer: \",\n additional_kwargs={\"documents\": x[\"documents\"]},\n )\n ]\n )\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -373,33 +209,7 @@ ] } ], - "source": [ - "### LLM fallback\n", - "\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Preamble\n", - "preamble = \"\"\"You are an assistant for question-answering tasks. Answer the question based upon your knowledge. Use three sentences maximum and keep the answer concise.\"\"\"\n", - "\n", - "# LLM\n", - "llm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n", - "\n", - "\n", - "# Prompt\n", - "def prompt(x):\n", - " return ChatPromptTemplate.from_messages(\n", - " [HumanMessage(f\"Question: {x['question']} \\nAnswer: \")]\n", - " )\n", - "\n", - "\n", - "# Chain\n", - "llm_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "question = \"Hi how are you?\"\n", - "generation = llm_chain.invoke({\"question\": question})\n", - "print(generation)" - ] + "source": ["### LLM fallback\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Preamble\npreamble = \"\"\"You are an assistant for question-answering tasks. Answer the question based upon your knowledge. Use three sentences maximum and keep the answer concise.\"\"\"\n\n# LLM\nllm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n\n\n# Prompt\ndef prompt(x):\n return ChatPromptTemplate.from_messages(\n [HumanMessage(f\"Question: {x['question']} \\nAnswer: \")]\n )\n\n\n# Chain\nllm_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"Hi how are you?\"\ngeneration = llm_chain.invoke({\"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -424,40 +234,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeHallucinations(BaseModel):\n", - " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# Preamble\n", - "preamble = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n\n", - "Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n", - "\n", - "# LLM with function call\n", - "llm = ChatCohere(model=\"command-r\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(\n", - " GradeHallucinations, preamble=preamble\n", - ")\n", - "\n", - "# Prompt\n", - "hallucination_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " # (\"system\", system),\n", - " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n", - " ]\n", - ")\n", - "\n", - "hallucination_grader = hallucination_prompt | structured_llm_grader\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# Preamble\npreamble = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n\nGive a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(\n GradeHallucinations, preamble=preamble\n)\n\n# Prompt\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n # (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -482,37 +259,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeAnswer(BaseModel):\n", - " \"\"\"Binary score to assess answer addresses question.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer addresses the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# Preamble\n", - "preamble = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n\n", - "Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n", - "\n", - "# LLM with function call\n", - "llm = ChatCohere(model=\"command-r\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeAnswer, preamble=preamble)\n", - "\n", - "# Prompt\n", - "answer_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n", - " ]\n", - ")\n", - "\n", - "answer_grader = answer_prompt | structured_llm_grader\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# Preamble\npreamble = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n\nGive a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer, preamble=preamble)\n\n# Prompt\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "markdown", @@ -532,14 +279,7 @@ "id": "01d829bb-1074-4976-b650-ead41dcb9788" }, "outputs": [], - "source": [ - "### Search\n", - "# os.environ['TAVILY_API_KEY'] =''\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "web_search_tool = TavilySearchResults()" - ] + "source": ["### Search\n# os.environ['TAVILY_API_KEY'] =''\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults()"] }, { "cell_type": "markdown", @@ -563,26 +303,7 @@ "id": "e723fcdb-06e6-402d-912e-899795b78408" }, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"|\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"|\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] }, { "cell_type": "markdown", @@ -602,220 +323,7 @@ "id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a" }, "outputs": [], - "source": [ - "from langchain.schema import Document\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.invoke(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def llm_fallback(state):\n", - " \"\"\"\n", - " Generate answer using the LLM w/o vectorstore\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---LLM Fallback---\")\n", - " question = state[\"question\"]\n", - " generation = llm_chain.invoke({\"question\": question})\n", - " return {\"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer using the vectorstore\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " if not isinstance(documents, list):\n", - " documents = [documents]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question}\n", - "\n", - "\n", - "def web_search(state):\n", - " \"\"\"\n", - " Web search based on the re-phrased question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with appended web results\n", - " \"\"\"\n", - "\n", - " print(\"---WEB SEARCH---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Web search\n", - " docs = web_search_tool.invoke({\"query\": question})\n", - " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", - " web_results = Document(page_content=web_results)\n", - "\n", - " return {\"documents\": web_results, \"question\": question}\n", - "\n", - "\n", - "### Edges ###\n", - "\n", - "\n", - "def route_question(state):\n", - " \"\"\"\n", - " Route question to web search or RAG.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ROUTE QUESTION---\")\n", - " question = state[\"question\"]\n", - " source = question_router.invoke({\"question\": question})\n", - "\n", - " # Fallback to LLM or raise error if no decision\n", - " if \"tool_calls\" not in source.additional_kwargs:\n", - " print(\"---ROUTE QUESTION TO LLM---\")\n", - " return \"llm_fallback\"\n", - " if len(source.additional_kwargs[\"tool_calls\"]) == 0:\n", - " raise \"Router could not decide source\"\n", - "\n", - " # Choose datasource\n", - " datasource = source.additional_kwargs[\"tool_calls\"][0][\"function\"][\"name\"]\n", - " if datasource == \"web_search\":\n", - " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", - " return \"web_search\"\n", - " elif datasource == \"vectorstore\":\n", - " print(\"---ROUTE QUESTION TO RAG---\")\n", - " return \"vectorstore\"\n", - " else:\n", - " print(\"---ROUTE QUESTION TO LLM---\")\n", - " return \"vectorstore\"\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " filtered_documents = state[\"documents\"]\n", - "\n", - " if not filtered_documents:\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, WEB SEARCH---\")\n", - " return \"web_search\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score.binary_score\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"" - ] + "source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef llm_fallback(state):\n \"\"\"\n Generate answer using the LLM w/o vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---LLM Fallback---\")\n question = state[\"question\"]\n generation = llm_chain.invoke({\"question\": question})\n return {\"question\": question, \"generation\": generation}\n\n\ndef generate(state):\n \"\"\"\n Generate answer using the vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n if not isinstance(documents, list):\n documents = [documents]\n\n # RAG generation\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n\n # Fallback to LLM or raise error if no decision\n if \"tool_calls\" not in source.additional_kwargs:\n print(\"---ROUTE QUESTION TO LLM---\")\n return \"llm_fallback\"\n if len(source.additional_kwargs[\"tool_calls\"]) == 0:\n raise \"Router could not decide source\"\n\n # Choose datasource\n datasource = source.additional_kwargs[\"tool_calls\"][0][\"function\"][\"name\"]\n if datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n else:\n print(\"---ROUTE QUESTION TO LLM---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, WEB SEARCH---\")\n return \"web_search\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] }, { "cell_type": "markdown", @@ -835,53 +343,7 @@ "id": "67854e07-9293-4c3c-bf9a-bc9a605570ee" }, "outputs": [], - "source": [ - "import pprint\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"web_search\", web_search) # web search\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # rag\n", - "workflow.add_node(\"llm_fallback\", llm_fallback) # llm\n", - "\n", - "# Build graph\n", - "workflow.set_conditional_entry_point(\n", - " route_question,\n", - " {\n", - " \"web_search\": \"web_search\",\n", - " \"vectorstore\": \"retrieve\",\n", - " \"llm_fallback\": \"llm_fallback\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"web_search\", \"generate\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"web_search\": \"web_search\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\", # Hallucinations: re-generate\n", - " \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search\n", - " \"useful\": END,\n", - " },\n", - ")\n", - "workflow.add_edge(\"llm_fallback\", END)\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["import pprint\n\nfrom langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"web_search\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # rag\nworkflow.add_node(\"llm_fallback\", llm_fallback) # llm\n\n# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"web_search\": \"web_search\",\n \"vectorstore\": \"retrieve\",\n \"llm_fallback\": \"llm_fallback\",\n })\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"web_search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\", # Hallucinations: re-generate\n \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search\n \"useful\": END,\n },\n)\nworkflow.add_edge(\"llm_fallback\", END)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "code", @@ -915,21 +377,7 @@ ] } ], - "source": [ - "# Run\n", - "inputs = {\n", - " \"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"\n", - "}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint.pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " pprint.pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint.pprint(value[\"generation\"])" - ] + "source": ["# Run\ninputs = {\n \"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -984,20 +432,7 @@ ] } ], - "source": [ - "# Run\n", - "inputs = {\"question\": \"What are the types of agent memory?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint.pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint.pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint.pprint(value[\"generation\"])" - ] + "source": ["# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -1033,20 +468,7 @@ ] } ], - "source": [ - "# Run\n", - "inputs = {\"question\": \"Hello, how are you today?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint.pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint.pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint.pprint(value[\"generation\"])" - ] + "source": ["# Run\ninputs = {\"question\": \"Hello, how are you today?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -1064,7 +486,7 @@ "id": "ce3cda0a-c4bd-41ea-830b-d992f27fde15", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_adaptive_rag_local.ipynb b/examples/rag/langgraph_adaptive_rag_local.ipynb index 44e73fb90..fcbc27c8b 100644 --- a/examples/rag/langgraph_adaptive_rag_local.ipynb +++ b/examples/rag/langgraph_adaptive_rag_local.ipynb @@ -44,10 +44,7 @@ "id": "88debf5c-6972-415c-b8fb-f65eab203b7a", "metadata": {}, "outputs": [], - "source": [ - "%capture --no-stderr\n", - "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]" - ] + "source": ["%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]"] }, { "cell_type": "markdown", @@ -79,10 +76,7 @@ "id": "af8379bd-7eae-4ba6-b632-12e89eab9920", "metadata": {}, "outputs": [], - "source": [ - "# Ollama model name\n", - "local_llm = \"mistral\"" - ] + "source": ["# Ollama model name\nlocal_llm = \"mistral\""] }, { "cell_type": "markdown", @@ -100,13 +94,7 @@ "id": "f6cb8dce-f580-421d-a05f-9fc71de2b023", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -122,34 +110,7 @@ "id": "f9ff6b99-080d-4827-b2cb-f775543d76f5", "metadata": {}, "outputs": [], - "source": [ - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_nomic.embeddings import NomicEmbeddings\n", - "\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=250, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -175,32 +136,7 @@ ] } ], - "source": [ - "### Router\n", - "\n", - "from langchain.prompts import PromptTemplate\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import JsonOutputParser\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n", - " Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n", - " You do not need to be stringent with the keywords in the question related to these topics. \\n\n", - " Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n", - " Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n", - " Question to route: {question}\"\"\",\n", - " input_variables=[\"question\"],\n", - ")\n", - "\n", - "question_router = prompt | llm | JsonOutputParser()\n", - "question = \"llm agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(question_router.invoke({\"question\": question}))" - ] + "source": ["### Router\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n You do not need to be stringent with the keywords in the question related to these topics. \\n\n Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n Question to route: {question}\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))"] }, { "cell_type": "code", @@ -216,33 +152,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "from langchain.prompts import PromptTemplate\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import JsonOutputParser\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", - " Here is the retrieved document: \\n\\n {document} \\n\\n\n", - " Here is the user question: {question} \\n\n", - " If the document contains keywords related to the user question, grade it as relevant. \\n\n", - " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n", - " input_variables=[\"question\", \"document\"],\n", - ")\n", - "\n", - "retrieval_grader = prompt | llm | JsonOutputParser()\n", - "question = \"agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "code", @@ -258,33 +168,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain import hub\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, temperature=0)\n", - "\n", - "\n", - "# Post-processing\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "question = \"agent memory\"\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -303,28 +187,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n", - " Here are the facts:\n", - " \\n ------- \\n\n", - " {documents} \n", - " \\n ------- \\n\n", - " Here is the answer: {generation}\n", - " Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", - " input_variables=[\"generation\", \"documents\"],\n", - ")\n", - "\n", - "hallucination_grader = prompt | llm | JsonOutputParser()\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -343,28 +206,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n", - " Here is the answer:\n", - " \\n ------- \\n\n", - " {generation} \n", - " \\n ------- \\n\n", - " Here is the question: {question}\n", - " Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", - " input_variables=[\"generation\", \"question\"],\n", - ")\n", - "\n", - "answer_grader = prompt | llm | JsonOutputParser()\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "code", @@ -383,23 +225,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Question Re-writer\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, temperature=0)\n", - "\n", - "# Prompt\n", - "re_write_prompt = PromptTemplate(\n", - " template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", - " for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n", - " Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n", - " input_variables=[\"generation\", \"question\"],\n", - ")\n", - "\n", - "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", - "question_rewriter.invoke({\"question\": question})" - ] + "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] }, { "cell_type": "markdown", @@ -415,13 +241,7 @@ "id": "6c3c1c70-ff84-41e8-bf72-738ed52f2dde", "metadata": {}, "outputs": [], - "source": [ - "### Search\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "web_search_tool = TavilySearchResults(k=3)" - ] + "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"] }, { "cell_type": "markdown", @@ -441,26 +261,7 @@ "id": "6e09087e-b2a9-437a-abee-129e426df799", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] }, { "cell_type": "code", @@ -468,216 +269,7 @@ "id": "7c5fa507-77ae-426a-a65f-f518b9525bd0", "metadata": {}, "outputs": [], - "source": [ - "### Nodes\n", - "\n", - "from langchain.schema import Document\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.get_relevant_documents(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score[\"score\"]\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question}\n", - "\n", - "\n", - "def transform_query(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates question key with a re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Re-write question\n", - " better_question = question_rewriter.invoke({\"question\": question})\n", - " return {\"documents\": documents, \"question\": better_question}\n", - "\n", - "\n", - "def web_search(state):\n", - " \"\"\"\n", - " Web search based on the re-phrased question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with appended web results\n", - " \"\"\"\n", - "\n", - " print(\"---WEB SEARCH---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Web search\n", - " docs = web_search_tool.invoke({\"query\": question})\n", - " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", - " web_results = Document(page_content=web_results)\n", - "\n", - " return {\"documents\": web_results, \"question\": question}\n", - "\n", - "\n", - "### Edges ###\n", - "\n", - "\n", - "def route_question(state):\n", - " \"\"\"\n", - " Route question to web search or RAG.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ROUTE QUESTION---\")\n", - " question = state[\"question\"]\n", - " print(question)\n", - " source = question_router.invoke({\"question\": question})\n", - " print(source)\n", - " print(source[\"datasource\"])\n", - " if source[\"datasource\"] == \"web_search\":\n", - " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", - " return \"web_search\"\n", - " elif source[\"datasource\"] == \"vectorstore\":\n", - " print(\"---ROUTE QUESTION TO RAG---\")\n", - " return \"vectorstore\"\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " filtered_documents = state[\"documents\"]\n", - "\n", - " if not filtered_documents:\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", - " )\n", - " return \"transform_query\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score[\"score\"]\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score[\"score\"]\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"" - ] + "source": ["### Nodes\n\nfrom langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] }, { "cell_type": "markdown", @@ -693,50 +285,7 @@ "id": "450eb313-ca75-4a43-b57e-7034bd3f40bf", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"web_search\", web_search) # web search\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", - "\n", - "# Build graph\n", - "workflow.set_conditional_entry_point(\n", - " route_question,\n", - " {\n", - " \"web_search\": \"web_search\",\n", - " \"vectorstore\": \"retrieve\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"web_search\", \"generate\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"transform_query\": \"transform_query\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"transform_query\", \"retrieve\")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\",\n", - " \"useful\": END,\n", - " \"not useful\": \"transform_query\",\n", - " },\n", - ")\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"web_search\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"web_search\": \"web_search\",\n \"vectorstore\": \"retrieve\",\n })\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "code", @@ -774,22 +323,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\"question\": \"What is the AlphaCodium paper about?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What is the AlphaCodium paper about?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -807,7 +341,7 @@ "id": "4620ede9-b014-499f-8acf-80f80ce0d944", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_agentic_rag.ipynb b/examples/rag/langgraph_agentic_rag.ipynb index dfe7a0777..486c6caed 100644 --- a/examples/rag/langgraph_agentic_rag.ipynb +++ b/examples/rag/langgraph_agentic_rag.ipynb @@ -20,10 +20,7 @@ "id": "969fb438", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters" - ] + "source": ["%%capture --no-stderr\n%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"] }, { "cell_type": "code", @@ -31,22 +28,7 @@ "id": "e4958a8c", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(key: str):\n", - " if key not in os.environ:\n", - " os.environ[key] = getpass.getpass(f\"{key}:\")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")\n", - "\n", - "# (Optional) For tracing\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(key: str):\n if key not in os.environ:\n os.environ[key] = getpass.getpass(f\"{key}:\")\n\n\n_set_env(\"OPENAI_API_KEY\")\n\n# (Optional) For tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -64,34 +46,7 @@ "id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", - "\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=100, chunk_overlap=50\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=OpenAIEmbeddings(),\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["from langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=100, chunk_overlap=50\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -107,17 +62,7 @@ "id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048", "metadata": {}, "outputs": [], - "source": [ - "from langchain.tools.retriever import create_retriever_tool\n", - "\n", - "retriever_tool = create_retriever_tool(\n", - " retriever,\n", - " \"retrieve_blog_posts\",\n", - " \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n", - ")\n", - "\n", - "tools = [retriever_tool]" - ] + "source": ["from langchain.tools.retriever import create_retriever_tool\n\nretriever_tool = create_retriever_tool(\n retriever,\n \"retrieve_blog_posts\",\n \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n)\n\ntools = [retriever_tool]"] }, { "cell_type": "markdown", @@ -141,19 +86,7 @@ "id": "0e378706-47d5-425a-8ba0-57b9acffbd0c", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " # The add_messages function defines how an update should be processed\n", - " # Default is to replace. add_messages says \"append\"\n", - " messages: Annotated[Sequence[BaseMessage], add_messages]" - ] + "source": ["from typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\nfrom langgraph.graph.message import add_messages\n\n\nclass AgentState(TypedDict):\n # The add_messages function defines how an update should be processed\n # Default is to replace. add_messages says \"append\"\n messages: Annotated[Sequence[BaseMessage], add_messages]"] }, { "attachments": { @@ -196,174 +129,7 @@ ] } ], - "source": [ - "from typing import Annotated, Literal, Sequence, TypedDict\n", - "\n", - "from langchain import hub\n", - "from langchain_core.messages import BaseMessage, HumanMessage\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "from langchain_core.prompts import PromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "from langgraph.prebuilt import tools_condition\n", - "\n", - "### Edges\n", - "\n", - "\n", - "def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (messages): The current state\n", - "\n", - " Returns:\n", - " str: A decision for whether the documents are relevant or not\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK RELEVANCE---\")\n", - "\n", - " # Data model\n", - " class grade(BaseModel):\n", - " \"\"\"Binary score for relevance check.\"\"\"\n", - "\n", - " binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n", - "\n", - " # LLM\n", - " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n", - "\n", - " # LLM with tool and validation\n", - " llm_with_tool = model.with_structured_output(grade)\n", - "\n", - " # Prompt\n", - " prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", - " Here is the retrieved document: \\n\\n {context} \\n\\n\n", - " Here is the user question: {question} \\n\n", - " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n", - " input_variables=[\"context\", \"question\"],\n", - " )\n", - "\n", - " # Chain\n", - " chain = prompt | llm_with_tool\n", - "\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - "\n", - " question = messages[0].content\n", - " docs = last_message.content\n", - "\n", - " scored_result = chain.invoke({\"question\": question, \"context\": docs})\n", - "\n", - " score = scored_result.binary_score\n", - "\n", - " if score == \"yes\":\n", - " print(\"---DECISION: DOCS RELEVANT---\")\n", - " return \"generate\"\n", - "\n", - " else:\n", - " print(\"---DECISION: DOCS NOT RELEVANT---\")\n", - " print(score)\n", - " return \"rewrite\"\n", - "\n", - "\n", - "### Nodes\n", - "\n", - "\n", - "def agent(state):\n", - " \"\"\"\n", - " Invokes the agent model to generate a response based on the current state. Given\n", - " the question, it will decide to retrieve using the retriever tool, or simply end.\n", - "\n", - " Args:\n", - " state (messages): The current state\n", - "\n", - " Returns:\n", - " dict: The updated state with the agent response appended to messages\n", - " \"\"\"\n", - " print(\"---CALL AGENT---\")\n", - " messages = state[\"messages\"]\n", - " model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n", - " model = model.bind_tools(tools)\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "def rewrite(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (messages): The current state\n", - "\n", - " Returns:\n", - " dict: The updated state with re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " messages = state[\"messages\"]\n", - " question = messages[0].content\n", - "\n", - " msg = [\n", - " HumanMessage(\n", - " content=f\"\"\" \\n \n", - " Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n", - " Here is the initial question:\n", - " \\n ------- \\n\n", - " {question} \n", - " \\n ------- \\n\n", - " Formulate an improved question: \"\"\",\n", - " )\n", - " ]\n", - "\n", - " # Grader\n", - " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n", - " response = model.invoke(msg)\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (messages): The current state\n", - "\n", - " Returns:\n", - " dict: The updated state with re-phrased question\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " messages = state[\"messages\"]\n", - " question = messages[0].content\n", - " last_message = messages[-1]\n", - "\n", - " question = messages[0].content\n", - " docs = last_message.content\n", - "\n", - " # Prompt\n", - " prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - " # LLM\n", - " llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n", - "\n", - " # Post-processing\n", - " def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - " # Chain\n", - " rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - " # Run\n", - " response = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n", - "prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like" - ] + "source": ["from typing import Annotated, Literal, Sequence, TypedDict\n\nfrom langchain import hub\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import tools_condition\n\n### Edges\n\n\ndef grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (messages): The current state\n\n Returns:\n str: A decision for whether the documents are relevant or not\n \"\"\"\n\n print(\"---CHECK RELEVANCE---\")\n\n # Data model\n class grade(BaseModel):\n \"\"\"Binary score for relevance check.\"\"\"\n\n binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n\n # LLM\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n\n # LLM with tool and validation\n llm_with_tool = model.with_structured_output(grade)\n\n # Prompt\n prompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {context} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n input_variables=[\"context\", \"question\"],\n )\n\n # Chain\n chain = prompt | llm_with_tool\n\n messages = state[\"messages\"]\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n scored_result = chain.invoke({\"question\": question, \"context\": docs})\n\n score = scored_result.binary_score\n\n if score == \"yes\":\n print(\"---DECISION: DOCS RELEVANT---\")\n return \"generate\"\n\n else:\n print(\"---DECISION: DOCS NOT RELEVANT---\")\n print(score)\n return \"rewrite\"\n\n\n### Nodes\n\n\ndef agent(state):\n \"\"\"\n Invokes the agent model to generate a response based on the current state. Given\n the question, it will decide to retrieve using the retriever tool, or simply end.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with the agent response appended to messages\n \"\"\"\n print(\"---CALL AGENT---\")\n messages = state[\"messages\"]\n model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n model = model.bind_tools(tools)\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef rewrite(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n messages = state[\"messages\"]\n question = messages[0].content\n\n msg = [\n HumanMessage(\n content=f\"\"\" \\n \n Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n Here is the initial question:\n \\n ------- \\n\n {question} \n \\n ------- \\n\n Formulate an improved question: \"\"\",\n )\n ]\n\n # Grader\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n response = model.invoke(msg)\n return {\"messages\": [response]}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n print(\"---GENERATE---\")\n messages = state[\"messages\"]\n question = messages[0].content\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n # Prompt\n prompt = hub.pull(\"rlm/rag-prompt\")\n\n # LLM\n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n\n # Post-processing\n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n # Chain\n rag_chain = prompt | llm | StrOutputParser()\n\n # Run\n response = rag_chain.invoke({\"context\": docs, \"question\": question})\n return {\"messages\": [response]}\n\n\nprint(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\nprompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"] }, { "cell_type": "markdown", @@ -384,48 +150,7 @@ "id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "from langgraph.prebuilt import ToolNode\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the nodes we will cycle between\n", - "workflow.add_node(\"agent\", agent) # agent\n", - "retrieve = ToolNode([retriever_tool])\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieval\n", - "workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n", - "workflow.add_node(\n", - " \"generate\", generate\n", - ") # Generating a response after we know the documents are relevant\n", - "# Call agent node to decide to retrieve or not\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# Decide whether to retrieve\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " # Assess agent decision\n", - " tools_condition,\n", - " {\n", - " # Translate the condition outputs to nodes in our graph\n", - " \"tools\": \"retrieve\",\n", - " END: END,\n", - " },\n", - ")\n", - "\n", - "# Edges taken after the `action` node is called.\n", - "workflow.add_conditional_edges(\n", - " \"retrieve\",\n", - " # Assess agent decision\n", - " grade_documents,\n", - ")\n", - "workflow.add_edge(\"generate\", END)\n", - "workflow.add_edge(\"rewrite\", \"agent\")\n", - "\n", - "# Compile\n", - "graph = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import ToolNode\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the nodes we will cycle between\nworkflow.add_node(\"agent\", agent) # agent\nretrieve = ToolNode([retriever_tool])\nworkflow.add_node(\"retrieve\", retrieve) # retrieval\nworkflow.add_node(\"rewrite\", rewrite) # Re-writing the question\nworkflow.add_node(\n \"generate\", generate\n) # Generating a response after we know the documents are relevant\n# Call agent node to decide to retrieve or not\nworkflow.add_edge(START, \"agent\")\n\n# Decide whether to retrieve\nworkflow.add_conditional_edges(\n \"agent\",\n # Assess agent decision\n tools_condition,\n {\n # Translate the condition outputs to nodes in our graph\n \"tools\": \"retrieve\",\n END: END,\n },\n)\n\n# Edges taken after the `action` node is called.\nworkflow.add_conditional_edges(\n \"retrieve\",\n # Assess agent decision\n grade_documents,\n)\nworkflow.add_edge(\"generate\", END)\nworkflow.add_edge(\"rewrite\", \"agent\")\n\n# Compile\ngraph = workflow.compile()"] }, { "cell_type": "code", @@ -444,15 +169,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "code", @@ -486,21 +203,7 @@ ] } ], - "source": [ - "import pprint\n", - "\n", - "inputs = {\n", - " \"messages\": [\n", - " (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n", - " ]\n", - "}\n", - "for output in graph.stream(inputs):\n", - " for key, value in output.items():\n", - " pprint.pprint(f\"Output from node '{key}':\")\n", - " pprint.pprint(\"---\")\n", - " pprint.pprint(value, indent=2, width=80, depth=None)\n", - " pprint.pprint(\"\\n---\\n\")" - ] + "source": ["import pprint\n\ninputs = {\n \"messages\": [\n (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n ]\n}\nfor output in graph.stream(inputs):\n for key, value in output.items():\n pprint.pprint(f\"Output from node '{key}':\")\n pprint.pprint(\"---\")\n pprint.pprint(value, indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -508,7 +211,7 @@ "id": "189333cc-5d34-4869-9f9b-741210e1096f", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_crag.ipynb b/examples/rag/langgraph_crag.ipynb index fbe536e6d..4cfbb07dc 100644 --- a/examples/rag/langgraph_crag.ipynb +++ b/examples/rag/langgraph_crag.ipynb @@ -47,9 +47,7 @@ "id": "568c84d6-9df6-4b7b-b50d-476c0a64a04b", "metadata": {}, "outputs": [], - "source": [ - "! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python" - ] + "source": ["! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python"] }, { "cell_type": "markdown", @@ -65,11 +63,7 @@ "id": "74710419-158d-4270-931c-de83db7b580d", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"\"" - ] + "source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -87,9 +81,7 @@ "id": "c3ac6e65-2d4e-48dd-9fff-40047373332d", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"TAVILY_API_KEY\"] = \"\"" - ] + "source": ["os.environ[\"TAVILY_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -107,11 +99,7 @@ "id": "e205f57e-5218-478b-ad8e-1723bdb0d45e", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -129,34 +117,7 @@ "id": "3a566a30-cf0e-4330-ad4d-9bf994bdfa86", "metadata": {}, "outputs": [], - "source": [ - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=250, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=OpenAIEmbeddings(),\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -180,44 +141,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "\n", - "# Data model\n", - "class GradeDocuments(BaseModel):\n", - " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", - " If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", - "grade_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", - " ]\n", - ")\n", - "\n", - "retrieval_grader = grade_prompt | structured_llm_grader\n", - "question = \"agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "code", @@ -233,31 +157,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain import hub\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", - "\n", - "\n", - "# Post-processing\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -276,28 +176,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Question Re-writer\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", - " for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n", - "re_write_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\n", - " \"human\",\n", - " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n", - " ),\n", - " ]\n", - ")\n", - "\n", - "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", - "question_rewriter.invoke({\"question\": question})" - ] + "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] }, { "cell_type": "markdown", @@ -313,13 +192,7 @@ "id": "46d51b53-54a9-4e0a-9f14-e39998f5b340", "metadata": {}, "outputs": [], - "source": [ - "### Search\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "web_search_tool = TavilySearchResults(k=3)" - ] + "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"] }, { "cell_type": "markdown", @@ -339,28 +212,7 @@ "id": "94b3945f-ef0f-458d-a443-f763903550b0", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " web_search: whether to add search\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " web_search: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n web_search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n web_search: str\n documents: List[str]"] }, { "cell_type": "code", @@ -368,155 +220,7 @@ "id": "efd639c5-82e2-45e6-a94a-6a4039646ef5", "metadata": {}, "outputs": [], - "source": [ - "from langchain.schema import Document\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.get_relevant_documents(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " web_search = \"No\"\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " web_search = \"Yes\"\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n", - "\n", - "\n", - "def transform_query(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates question key with a re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Re-write question\n", - " better_question = question_rewriter.invoke({\"question\": question})\n", - " return {\"documents\": documents, \"question\": better_question}\n", - "\n", - "\n", - "def web_search(state):\n", - " \"\"\"\n", - " Web search based on the re-phrased question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with appended web results\n", - " \"\"\"\n", - "\n", - " print(\"---WEB SEARCH---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Web search\n", - " docs = web_search_tool.invoke({\"query\": question})\n", - " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", - " web_results = Document(page_content=web_results)\n", - " documents.append(web_results)\n", - "\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "### Edges\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " web_search = state[\"web_search\"]\n", - " state[\"documents\"]\n", - "\n", - " if web_search == \"Yes\":\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", - " )\n", - " return \"transform_query\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"" - ] + "source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n web_search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n web_search = \"Yes\"\n continue\n return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n documents.append(web_results)\n\n return {\"documents\": documents, \"question\": question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n web_search = state[\"web_search\"]\n state[\"documents\"]\n\n if web_search == \"Yes\":\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\""] }, { "cell_type": "markdown", @@ -534,36 +238,7 @@ "id": "dedae17a-98c6-474d-90a7-9234b7c8cea0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", - "workflow.add_node(\"web_search_node\", web_search) # web search\n", - "\n", - "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"transform_query\": \"transform_query\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"transform_query\", \"web_search_node\")\n", - "workflow.add_edge(\"web_search_node\", \"generate\")\n", - "workflow.add_edge(\"generate\", END)\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\nworkflow.add_node(\"web_search_node\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"web_search_node\")\nworkflow.add_edge(\"web_search_node\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "code", @@ -606,22 +281,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\"question\": \"What are the types of agent memory?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "code", @@ -666,22 +326,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\"question\": \"How does the AlphaCodium paper work?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"How does the AlphaCodium paper work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -701,7 +346,7 @@ "id": "6ce65be5-fd12-4ffc-984c-34c132693e69", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_crag_local.ipynb b/examples/rag/langgraph_crag_local.ipynb index 1c1127045..adc218ac8 100644 --- a/examples/rag/langgraph_crag_local.ipynb +++ b/examples/rag/langgraph_crag_local.ipynb @@ -56,10 +56,7 @@ "id": "4a660963-bd3d-4c87-b2e4-b6e432055211", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai"] }, { "cell_type": "code", @@ -67,11 +64,7 @@ "id": "68316ba0-854b-41e1-9af5-1f9e965946e3", "metadata": {}, "outputs": [], - "source": [ - "# Search\n", - "import os\n", - "os.environ[\"TAVILY_API_KEY\"] = \"xxx\"" - ] + "source": ["# Search\nimport os\nos.environ[\"TAVILY_API_KEY\"] = \"xxx\""] }, { "cell_type": "code", @@ -79,10 +72,7 @@ "id": "0be68860-dded-481e-9fc7-a5042bf92c04", "metadata": {}, "outputs": [], - "source": [ - "# Embedding (optional)\n", - "os.environ[\"OPENAI_API_KEY\"] = \"xxx\"" - ] + "source": ["# Embedding (optional)\nos.environ[\"OPENAI_API_KEY\"] = \"xxx\""] }, { "cell_type": "code", @@ -90,13 +80,7 @@ "id": "7248ab88-2b97-41eb-8dbb-4ea65525ed9a", "metadata": {}, "outputs": [], - "source": [ - "# Tracing and testing (optional)\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\"" - ] + "source": ["# Tracing and testing (optional)\nos.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\""] }, { "cell_type": "markdown", @@ -114,11 +98,7 @@ "id": "2f4db331-c4d0-4c7c-a9a5-0bebc8a89c6c", "metadata": {}, "outputs": [], - "source": [ - "local_llm = \"llama3\"\n", - "model_tested = \"llama3-8b\"\n", - "metadata = f\"CRAG, {model_tested}\"" - ] + "source": ["local_llm = \"llama3\"\nmodel_tested = \"llama3-8b\"\nmetadata = f\"CRAG, {model_tested}\""] }, { "cell_type": "markdown", @@ -144,48 +124,7 @@ ] } ], - "source": [ - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import SKLearnVectorStore\n", - "from langchain_nomic.embeddings import NomicEmbeddings # local\n", - "from langchain_openai import OpenAIEmbeddings # api\n", - "\n", - "# List of URLs to load documents from\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "# Load documents from the URLs\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "# Initialize a text splitter with specified chunk size and overlap\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=250, chunk_overlap=0\n", - ")\n", - "\n", - "# Split the documents into chunks\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Embedding\n", - "'''\n", - "embedding=NomicEmbeddings(\n", - " model=\"nomic-embed-text-v1.5\",\n", - " inference_mode=\"local\",\n", - ")\n", - "'''\n", - "embedding = OpenAIEmbeddings()\n", - "\n", - "# Add the document chunks to the \"vector store\"\n", - "vectorstore = SKLearnVectorStore.from_documents(\n", - " documents=doc_splits,\n", - " embedding=embedding,\n", - ")\n", - "retriever = vectorstore.as_retriever(k=4)" - ] + "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_nomic.embeddings import NomicEmbeddings # local\nfrom langchain_openai import OpenAIEmbeddings # api\n\n# List of URLs to load documents from\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load documents from the URLs\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Initialize a text splitter with specified chunk size and overlap\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\n\n# Split the documents into chunks\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Embedding\n'''\nembedding=NomicEmbeddings(\n model=\"nomic-embed-text-v1.5\",\n inference_mode=\"local\",\n)\n'''\nembedding = OpenAIEmbeddings()\n\n# Add the document chunks to the \"vector store\"\nvectorstore = SKLearnVectorStore.from_documents(\n documents=doc_splits,\n embedding=embedding,\n)\nretriever = vectorstore.as_retriever(k=4)"] }, { "attachments": {}, @@ -210,47 +149,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "from langchain.prompts import PromptTemplate\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import JsonOutputParser\n", - "from langchain_mistralai.chat_models import ChatMistralAI\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a teacher grading a quiz. You will be given: \n", - " 1/ a QUESTION\n", - " 2/ A FACT provided by the student\n", - " \n", - " You are grading RELEVANCE RECALL:\n", - " A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n", - " A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n", - " 1 is the highest (best) score. 0 is the lowest score you can give. \n", - " \n", - " Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n", - " \n", - " Avoid simply stating the correct answer at the outset.\n", - " \n", - " Question: {question} \\n\n", - " Fact: \\n\\n {documents} \\n\\n\n", - " \n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n", - " \"\"\",\n", - " input_variables=[\"question\", \"documents\"],\n", - ")\n", - "\n", - "retrieval_grader = prompt | llm | JsonOutputParser()\n", - "question = \"agent memory\"\n", - "docs = retriever.invoke(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_mistralai.chat_models import ChatMistralAI\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a teacher grading a quiz. You will be given: \n 1/ a QUESTION\n 2/ A FACT provided by the student\n \n You are grading RELEVANCE RECALL:\n A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n 1 is the highest (best) score. 0 is the lowest score you can give. \n \n Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n \n Avoid simply stating the correct answer at the outset.\n \n Question: {question} \\n\n Fact: \\n\\n {documents} \\n\\n\n \n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))"] }, { "cell_type": "code", @@ -266,37 +165,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are an assistant for question-answering tasks. \n", - " \n", - " Use the following documents to answer the question. \n", - " \n", - " If you don't know the answer, just say that you don't know. \n", - " \n", - " Use three sentences maximum and keep the answer concise:\n", - " Question: {question} \n", - " Documents: {documents} \n", - " Answer: \n", - " \"\"\",\n", - " input_variables=[\"question\", \"documents\"],\n", - ")\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, temperature=0)\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"documents\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are an assistant for question-answering tasks. \n \n Use the following documents to answer the question. \n \n If you don't know the answer, just say that you don't know. \n \n Use three sentences maximum and keep the answer concise:\n Question: {question} \n Documents: {documents} \n Answer: \n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -304,13 +173,7 @@ "id": "b36a2f36-bc5f-408d-a5e8-3fa203c233f6", "metadata": {}, "outputs": [], - "source": [ - "### Search\n", - "\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "web_search_tool = TavilySearchResults(k=3)" - ] + "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"] }, { "cell_type": "markdown", @@ -339,177 +202,7 @@ "output_type": "display_data" } ], - "source": [ - "from typing import List\n", - "from typing_extensions import TypedDict\n", - "from IPython.display import Image, display\n", - "from langchain.schema import Document\n", - "from langgraph.graph import START, END, StateGraph\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " search: whether to add search\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " search: str\n", - " documents: List[str]\n", - " steps: List[str]\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " question = state[\"question\"]\n", - " documents = retriever.invoke(question)\n", - " steps = state[\"steps\"]\n", - " steps.append(\"retrieve_documents\")\n", - " return {\"documents\": documents, \"question\": question, \"steps\": steps}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - "\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n", - " steps = state[\"steps\"]\n", - " steps.append(\"generate_answer\")\n", - " return {\n", - " \"documents\": documents,\n", - " \"question\": question,\n", - " \"generation\": generation,\n", - " \"steps\": steps,\n", - " }\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " steps = state[\"steps\"]\n", - " steps.append(\"grade_document_retrieval\")\n", - " filtered_docs = []\n", - " search = \"No\"\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"documents\": d.page_content}\n", - " )\n", - " grade = score[\"score\"]\n", - " if grade == \"yes\":\n", - " filtered_docs.append(d)\n", - " else:\n", - " search = \"Yes\"\n", - " continue\n", - " return {\n", - " \"documents\": filtered_docs,\n", - " \"question\": question,\n", - " \"search\": search,\n", - " \"steps\": steps,\n", - " }\n", - "\n", - "\n", - "def web_search(state):\n", - " \"\"\"\n", - " Web search based on the re-phrased question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with appended web results\n", - " \"\"\"\n", - "\n", - " question = state[\"question\"]\n", - " documents = state.get(\"documents\", [])\n", - " steps = state[\"steps\"]\n", - " steps.append(\"web_search\")\n", - " web_results = web_search_tool.invoke({\"query\": question})\n", - " documents.extend(\n", - " [\n", - " Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n", - " for d in web_results\n", - " ]\n", - " )\n", - " return {\"documents\": documents, \"question\": question, \"steps\": steps}\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - " search = state[\"search\"]\n", - " if search == \"Yes\":\n", - " return \"search\"\n", - " else:\n", - " return \"generate\"\n", - "\n", - "\n", - "# Graph\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"web_search\", web_search) # web search\n", - "\n", - "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"search\": \"web_search\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"web_search\", \"generate\")\n", - "workflow.add_edge(\"generate\", END)\n", - "\n", - "custom_graph = workflow.compile()\n", - "\n", - "display(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from typing import List\nfrom typing_extensions import TypedDict\nfrom IPython.display import Image, display\nfrom langchain.schema import Document\nfrom langgraph.graph import START, END, StateGraph\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n search: str\n documents: List[str]\n steps: List[str]\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n question = state[\"question\"]\n documents = retriever.invoke(question)\n steps = state[\"steps\"]\n steps.append(\"retrieve_documents\")\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n steps = state[\"steps\"]\n steps.append(\"generate_answer\")\n return {\n \"documents\": documents,\n \"question\": question,\n \"generation\": generation,\n \"steps\": steps,\n }\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n steps = state[\"steps\"]\n steps.append(\"grade_document_retrieval\")\n filtered_docs = []\n search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"documents\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n filtered_docs.append(d)\n else:\n search = \"Yes\"\n continue\n return {\n \"documents\": filtered_docs,\n \"question\": question,\n \"search\": search,\n \"steps\": steps,\n }\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n question = state[\"question\"]\n documents = state.get(\"documents\", [])\n steps = state[\"steps\"]\n steps.append(\"web_search\")\n web_results = web_search_tool.invoke({\"query\": question})\n documents.extend(\n [\n Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n for d in web_results\n ]\n )\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n search = state[\"search\"]\n if search == \"Yes\":\n return \"search\"\n else:\n return \"generate\"\n\n\n# Graph\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"web_search\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\ncustom_graph = workflow.compile()\n\ndisplay(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "code", @@ -532,21 +225,7 @@ "output_type": "execute_result" } ], - "source": [ - "import uuid\n", - "\n", - "def predict_custom_agent_local_answer(example: dict):\n", - " config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n", - " state_dict = custom_graph.invoke(\n", - " {\"question\": example[\"input\"], \"steps\": []}, config\n", - " )\n", - " return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n", - "\n", - "\n", - "example = {\"input\": \"What are the types of agent memory?\"}\n", - "response = predict_custom_agent_local_answer(example)\n", - "response" - ] + "source": ["import uuid\n\ndef predict_custom_agent_local_answer(example: dict):\n config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n state_dict = custom_graph.invoke(\n {\"question\": example[\"input\"], \"steps\": []}, config\n )\n return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n\n\nexample = {\"input\": \"What are the types of agent memory?\"}\nresponse = predict_custom_agent_local_answer(example)\nresponse"] }, { "cell_type": "markdown", @@ -582,41 +261,7 @@ "id": "b83706ac-724b-46b1-9f08-66e6c4fac742", "metadata": {}, "outputs": [], - "source": [ - "from langsmith import Client\n", - "\n", - "client = Client()\n", - "\n", - "# Create a dataset\n", - "examples = [\n", - " (\n", - " \"How does the ReAct agent use self-reflection? \",\n", - " \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n", - " ),\n", - " (\n", - " \"What are the types of biases that can arise with few-shot prompting?\",\n", - " \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n", - " ),\n", - " (\n", - " \"What are five types of adversarial attacks?\",\n", - " \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n", - " ),\n", - " (\n", - " \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n", - " \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n", - " ),\n", - " (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n", - "]\n", - "\n", - "# Save it\n", - "dataset_name = \"Corrective RAG Agent Testing\"\n", - "if not client.has_dataset(dataset_name=dataset_name):\n", - " dataset = client.create_dataset(dataset_name=dataset_name)\n", - " inputs, outputs = zip(\n", - " *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n", - " )\n", - " client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)" - ] + "source": ["from langsmith import Client\n\nclient = Client()\n\n# Create a dataset\nexamples = [\n (\n \"How does the ReAct agent use self-reflection? \",\n \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n ),\n (\n \"What are the types of biases that can arise with few-shot prompting?\",\n \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n ),\n (\n \"What are five types of adversarial attacks?\",\n \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n ),\n (\n \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n ),\n (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n]\n\n# Save it\ndataset_name = \"Corrective RAG Agent Testing\"\nif not client.has_dataset(dataset_name=dataset_name):\n dataset = client.create_dataset(dataset_name=dataset_name)\n inputs, outputs = zip(\n *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n )\n client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)"] }, { "cell_type": "markdown", @@ -636,38 +281,7 @@ "id": "0a63776c-f9cd-46ce-b8cf-95c066dc5b06", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "# Grade prompt\n", - "grade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n", - "\n", - "def answer_evaluator(run, example) -> dict:\n", - " \"\"\"\n", - " A simple evaluator for RAG answer accuracy\n", - " \"\"\"\n", - "\n", - " # Get the question, the ground truth reference answer, RAG chain answer prediction\n", - " input_question = example.inputs[\"input\"]\n", - " reference = example.outputs[\"output\"]\n", - " prediction = run.outputs[\"response\"]\n", - "\n", - " # Define an LLM grader\n", - " llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", - " answer_grader = grade_prompt_answer_accuracy | llm\n", - "\n", - " # Run evaluator\n", - " score = answer_grader.invoke(\n", - " {\n", - " \"question\": input_question,\n", - " \"correct_answer\": reference,\n", - " \"student_answer\": prediction,\n", - " }\n", - " )\n", - " score = score[\"Score\"]\n", - " return {\"key\": \"answer_v_reference_score\", \"score\": score}" - ] + "source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\n# Grade prompt\ngrade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n\ndef answer_evaluator(run, example) -> dict:\n \"\"\"\n A simple evaluator for RAG answer accuracy\n \"\"\"\n\n # Get the question, the ground truth reference answer, RAG chain answer prediction\n input_question = example.inputs[\"input\"]\n reference = example.outputs[\"output\"]\n prediction = run.outputs[\"response\"]\n\n # Define an LLM grader\n llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n answer_grader = grade_prompt_answer_accuracy | llm\n\n # Run evaluator\n score = answer_grader.invoke(\n {\n \"question\": input_question,\n \"correct_answer\": reference,\n \"student_answer\": prediction,\n }\n )\n score = score[\"Score\"]\n return {\"key\": \"answer_v_reference_score\", \"score\": score}"] }, { "cell_type": "markdown", @@ -687,50 +301,7 @@ "id": "deb28175-27a1-4afc-9747-2983e87fc881", "metadata": {}, "outputs": [], - "source": [ - "from langsmith.schemas import Example, Run\n", - "\n", - "# Reasoning traces that we expect the agents to take\n", - "expected_trajectory_1 = [\n", - " \"retrieve_documents\",\n", - " \"grade_document_retrieval\",\n", - " \"web_search\",\n", - " \"generate_answer\",\n", - "]\n", - "expected_trajectory_2 = [\n", - " \"retrieve_documents\",\n", - " \"grade_document_retrieval\",\n", - " \"generate_answer\",\n", - "]\n", - "\n", - "def check_trajectory_react(root_run: Run, example: Example) -> dict:\n", - " \"\"\"\n", - " Check if all expected tools are called in exact order and without any additional tool calls.\n", - " \"\"\"\n", - " messages = root_run.outputs[\"messages\"]\n", - " tool_calls = find_tool_calls_react(messages)\n", - " print(f\"Tool calls ReAct agent: {tool_calls}\")\n", - " if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n", - " score = 1\n", - " else:\n", - " score = 0\n", - "\n", - " return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n", - "\n", - "\n", - "def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n", - " \"\"\"\n", - " Check if all expected tools are called in exact order and without any additional tool calls.\n", - " \"\"\"\n", - " tool_calls = root_run.outputs[\"steps\"]\n", - " print(f\"Tool calls custom agent: {tool_calls}\")\n", - " if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n", - " score = 1\n", - " else:\n", - " score = 0\n", - "\n", - " return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}" - ] + "source": ["from langsmith.schemas import Example, Run\n\n# Reasoning traces that we expect the agents to take\nexpected_trajectory_1 = [\n \"retrieve_documents\",\n \"grade_document_retrieval\",\n \"web_search\",\n \"generate_answer\",\n]\nexpected_trajectory_2 = [\n \"retrieve_documents\",\n \"grade_document_retrieval\",\n \"generate_answer\",\n]\n\ndef check_trajectory_react(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n messages = root_run.outputs[\"messages\"]\n tool_calls = find_tool_calls_react(messages)\n print(f\"Tool calls ReAct agent: {tool_calls}\")\n if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n\n\ndef check_trajectory_custom(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n tool_calls = root_run.outputs[\"steps\"]\n print(f\"Tool calls custom agent: {tool_calls}\")\n if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}"] }, { "cell_type": "code", @@ -784,20 +355,7 @@ ] } ], - "source": [ - "from langsmith.evaluation import evaluate\n", - "\n", - "experiment_prefix = f\"custom-agent-{model_tested}\"\n", - "experiment_results = evaluate(\n", - " predict_custom_agent_local_answer,\n", - " data=dataset_name,\n", - " evaluators=[answer_evaluator, check_trajectory_custom],\n", - " experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n", - " num_repetitions=3,\n", - " max_concurrency=1, # Use when running locally\n", - " metadata={\"version\": metadata},\n", - ")" - ] + "source": ["from langsmith.evaluation import evaluate\n\nexperiment_prefix = f\"custom-agent-{model_tested}\"\nexperiment_results = evaluate(\n predict_custom_agent_local_answer,\n data=dataset_name,\n evaluators=[answer_evaluator, check_trajectory_custom],\n experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n num_repetitions=3,\n max_concurrency=1, # Use when running locally\n metadata={\"version\": metadata},\n)"] }, { "attachments": { @@ -824,7 +382,7 @@ "id": "79295798-0181-417e-abad-11dddb6ff05e", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_rag_agent_llama3_local.ipynb b/examples/rag/langgraph_rag_agent_llama3_local.ipynb index f118ce137..2046cea6e 100644 --- a/examples/rag/langgraph_rag_agent_llama3_local.ipynb +++ b/examples/rag/langgraph_rag_agent_llama3_local.ipynb @@ -49,10 +49,7 @@ "id": "21e597f9", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local] langchain-text-splitters" - ] + "source": ["%%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local] langchain-text-splitters"] }, { "cell_type": "markdown", @@ -68,13 +65,7 @@ "id": "333cbcf4", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "code", @@ -82,11 +73,7 @@ "id": "2096d49c-d3dc-4329-ada7-aff56d210198", "metadata": {}, "outputs": [], - "source": [ - "### LLM\n", - "\n", - "local_llm = \"llama3\"" - ] + "source": ["### LLM\n\nlocal_llm = \"llama3\""] }, { "cell_type": "code", @@ -94,36 +81,7 @@ "id": "267c63e1-4c2f-439d-8d95-4c6aa01f41cf", "metadata": {}, "outputs": [], - "source": [ - "### Index\n", - "\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_nomic.embeddings import NomicEmbeddings\n", - "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", - "\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=250, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["### Index\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "code", @@ -139,35 +97,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import JsonOutputParser\n", - "from langchain_core.prompts import PromptTemplate\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing relevance \n", - " of a retrieved document to a user question. If the document contains keywords related to the user question, \n", - " grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n", - " <|eot_id|><|start_header_id|>user<|end_header_id|>\n", - " Here is the retrieved document: \\n\\n {document} \\n\\n\n", - " Here is the user question: {question} \\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n", - " \"\"\",\n", - " input_variables=[\"question\", \"document\"],\n", - ")\n", - "\n", - "retrieval_grader = prompt | llm | JsonOutputParser()\n", - "question = \"agent memory\"\n", - "docs = retriever.invoke(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing relevance \n of a retrieved document to a user question. If the document contains keywords related to the user question, \n grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n <|eot_id|><|start_header_id|>user<|end_header_id|>\n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n \"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "code", @@ -183,40 +113,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "from langchain_core.prompts import PromptTemplate\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an assistant for question-answering tasks. \n", - " Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. \n", - " Use three sentences maximum and keep the answer concise <|eot_id|><|start_header_id|>user<|end_header_id|>\n", - " Question: {question} \n", - " Context: {context} \n", - " Answer: <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", - " input_variables=[\"question\", \"document\"],\n", - ")\n", - "\n", - "llm = ChatOllama(model=local_llm, temperature=0)\n", - "\n", - "\n", - "# Post-processing\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "question = \"agent memory\"\n", - "docs = retriever.invoke(question)\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an assistant for question-answering tasks. \n Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. \n Use three sentences maximum and keep the answer concise <|eot_id|><|start_header_id|>user<|end_header_id|>\n Question: {question} \n Context: {context} \n Answer: <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -235,29 +132,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\" <|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether \n", - " an answer is grounded in / supported by a set of facts. Give a binary 'yes' or 'no' score to indicate \n", - " whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a \n", - " single key 'score' and no preamble or explanation. <|eot_id|><|start_header_id|>user<|end_header_id|>\n", - " Here are the facts:\n", - " \\n ------- \\n\n", - " {documents} \n", - " \\n ------- \\n\n", - " Here is the answer: {generation} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", - " input_variables=[\"generation\", \"documents\"],\n", - ")\n", - "\n", - "hallucination_grader = prompt | llm | JsonOutputParser()\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\" <|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether \n an answer is grounded in / supported by a set of facts. Give a binary 'yes' or 'no' score to indicate \n whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a \n single key 'score' and no preamble or explanation. <|eot_id|><|start_header_id|>user<|end_header_id|>\n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -276,28 +151,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether an \n", - " answer is useful to resolve a question. Give a binary score 'yes' or 'no' to indicate whether the answer is \n", - " useful to resolve a question. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\n", - " <|eot_id|><|start_header_id|>user<|end_header_id|> Here is the answer:\n", - " \\n ------- \\n\n", - " {generation} \n", - " \\n ------- \\n\n", - " Here is the question: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", - " input_variables=[\"generation\", \"question\"],\n", - ")\n", - "\n", - "answer_grader = prompt | llm | JsonOutputParser()\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether an \n answer is useful to resolve a question. Give a binary score 'yes' or 'no' to indicate whether the answer is \n useful to resolve a question. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\n <|eot_id|><|start_header_id|>user<|end_header_id|> Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "code", @@ -313,32 +167,7 @@ ] } ], - "source": [ - "### Router\n", - "\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import JsonOutputParser\n", - "from langchain_core.prompts import PromptTemplate\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an expert at routing a \n", - " user question to a vectorstore or web search. Use the vectorstore for questions on LLM agents, \n", - " prompt engineering, and adversarial attacks. You do not need to be stringent with the keywords \n", - " in the question related to these topics. Otherwise, use web-search. Give a binary choice 'web_search' \n", - " or 'vectorstore' based on the question. Return the a JSON with a single key 'datasource' and \n", - " no premable or explanation. Question to route: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n", - " input_variables=[\"question\"],\n", - ")\n", - "\n", - "question_router = prompt | llm | JsonOutputParser()\n", - "question = \"llm agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(question_router.invoke({\"question\": question}))" - ] + "source": ["### Router\n\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an expert at routing a \n user question to a vectorstore or web search. Use the vectorstore for questions on LLM agents, \n prompt engineering, and adversarial attacks. You do not need to be stringent with the keywords \n in the question related to these topics. Otherwise, use web-search. Give a binary choice 'web_search' \n or 'vectorstore' based on the question. Return the a JSON with a single key 'datasource' and \n no premable or explanation. Question to route: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))"] }, { "cell_type": "code", @@ -346,12 +175,7 @@ "id": "023ff2db-eb4e-4d44-904c-ea061abc16d9", "metadata": {}, "outputs": [], - "source": [ - "### Search\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "web_search_tool = TavilySearchResults(k=3)" - ] + "source": ["### Search\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"] }, { "cell_type": "markdown", @@ -367,246 +191,7 @@ "id": "07fa3d08-6a86-4705-a28b-e2721070bc5e", "metadata": {}, "outputs": [], - "source": [ - "from pprint import pprint\n", - "from typing import List\n", - "\n", - "from langchain_core.documents import Document\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "### State\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " web_search: whether to add search\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " web_search: str\n", - " documents: List[str]\n", - "\n", - "\n", - "### Nodes\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents from vectorstore\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.invoke(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer using RAG on retrieved documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question\n", - " If any document is not relevant, we will set a flag to run web search\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Filtered out irrelevant documents and updated web_search state\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " web_search = \"No\"\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score[\"score\"]\n", - " # Document relevant\n", - " if grade.lower() == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " # Document not relevant\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " # We do not include the document in filtered_docs\n", - " # We set a flag to indicate that we want to run web search\n", - " web_search = \"Yes\"\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n", - "\n", - "\n", - "def web_search(state):\n", - " \"\"\"\n", - " Web search based based on the question\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Appended web results to documents\n", - " \"\"\"\n", - "\n", - " print(\"---WEB SEARCH---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Web search\n", - " docs = web_search_tool.invoke({\"query\": question})\n", - " web_results = \"\\n\".join([d[\"content\"] for d in docs])\n", - " web_results = Document(page_content=web_results)\n", - " if documents is not None:\n", - " documents.append(web_results)\n", - " else:\n", - " documents = [web_results]\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "### Conditional edge\n", - "\n", - "\n", - "def route_question(state):\n", - " \"\"\"\n", - " Route question to web search or RAG.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ROUTE QUESTION---\")\n", - " question = state[\"question\"]\n", - " print(question)\n", - " source = question_router.invoke({\"question\": question})\n", - " print(source)\n", - " print(source[\"datasource\"])\n", - " if source[\"datasource\"] == \"web_search\":\n", - " print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n", - " return \"websearch\"\n", - " elif source[\"datasource\"] == \"vectorstore\":\n", - " print(\"---ROUTE QUESTION TO RAG---\")\n", - " return \"vectorstore\"\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or add web search\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " web_search = state[\"web_search\"]\n", - " state[\"documents\"]\n", - "\n", - " if web_search == \"Yes\":\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n", - " )\n", - " return \"websearch\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "### Conditional edge\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score[\"score\"]\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score[\"score\"]\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"\n", - "\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"websearch\", web_search) # web search\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae" - ] + "source": ["from pprint import pprint\nfrom typing import List\n\nfrom langchain_core.documents import Document\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n### State\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n web_search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n web_search: str\n documents: List[str]\n\n\n### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents from vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer using RAG on retrieved documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question\n If any document is not relevant, we will set a flag to run web search\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Filtered out irrelevant documents and updated web_search state\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n web_search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n # Document relevant\n if grade.lower() == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n # Document not relevant\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n # We do not include the document in filtered_docs\n # We set a flag to indicate that we want to run web search\n web_search = \"Yes\"\n continue\n return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n\n\ndef web_search(state):\n \"\"\"\n Web search based based on the question\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Appended web results to documents\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n if documents is not None:\n documents.append(web_results)\n else:\n documents = [web_results]\n return {\"documents\": documents, \"question\": question}\n\n\n### Conditional edge\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"websearch\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or add web search\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n web_search = state[\"web_search\"]\n state[\"documents\"]\n\n if web_search == \"Yes\":\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n )\n return \"websearch\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\n### Conditional edge\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\"\n\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"websearch\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae"] }, { "cell_type": "markdown", @@ -622,36 +207,7 @@ "id": "d9a4b9e4-3ba8-47d6-958c-e5a7112ac6f4", "metadata": {}, "outputs": [], - "source": [ - "# Build graph\n", - "workflow.set_conditional_entry_point(\n", - " route_question,\n", - " {\n", - " \"websearch\": \"websearch\",\n", - " \"vectorstore\": \"retrieve\",\n", - " },\n", - ")\n", - "\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"websearch\": \"websearch\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"websearch\", \"generate\")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\",\n", - " \"useful\": END,\n", - " \"not useful\": \"websearch\",\n", - " },\n", - ")" - ] + "source": ["# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"websearch\": \"websearch\",\n \"vectorstore\": \"retrieve\",\n })\n\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"websearch\": \"websearch\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"websearch\", \"generate\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"websearch\",\n },\n)"] }, { "cell_type": "code", @@ -698,18 +254,7 @@ ] } ], - "source": [ - "# Compile\n", - "app = workflow.compile()\n", - "\n", - "# Test\n", - "\n", - "inputs = {\"question\": \"What are the types of agent memory?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " pprint(f\"Finished running: {key}:\")\n", - "pprint(value[\"generation\"])" - ] + "source": ["# Compile\napp = workflow.compile()\n\n# Test\n\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n pprint(f\"Finished running: {key}:\")\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -749,17 +294,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Compile\n", - "app = workflow.compile()\n", - "inputs = {\"question\": \"Who are the Bears expected to draft first in the NFL draft?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " pprint(f\"Finished running: {key}:\")\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Compile\napp = workflow.compile()\ninputs = {\"question\": \"Who are the Bears expected to draft first in the NFL draft?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n pprint(f\"Finished running: {key}:\")\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -777,7 +312,7 @@ "id": "1da64a95-736b-4373-8fb4-6ed4bf60a647", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_self_rag.ipynb b/examples/rag/langgraph_self_rag.ipynb index a28e50dfb..cb2d5d934 100644 --- a/examples/rag/langgraph_self_rag.ipynb +++ b/examples/rag/langgraph_self_rag.ipynb @@ -59,9 +59,7 @@ "id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9", "metadata": {}, "outputs": [], - "source": [ - "! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph" - ] + "source": ["! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph"] }, { "cell_type": "markdown", @@ -77,11 +75,7 @@ "id": "f18b63c7-d0d3-41c1-ae6b-5a0f1b8ccf0f", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"\"" - ] + "source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -99,11 +93,7 @@ "id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -121,34 +111,7 @@ "id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d", "metadata": {}, "outputs": [], - "source": [ - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=250, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=OpenAIEmbeddings(),\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -180,46 +143,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "\n", - "# Data model\n", - "class GradeDocuments(BaseModel):\n", - " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", - " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", - " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n", - "grade_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n", - " ]\n", - ")\n", - "\n", - "retrieval_grader = grade_prompt | structured_llm_grader\n", - "question = \"agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "code", @@ -235,31 +159,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain import hub\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", - "\n", - "\n", - "# Post-processing\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -278,36 +178,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeHallucinations(BaseModel):\n", - " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n", - " Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n", - "hallucination_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n", - " ]\n", - ")\n", - "\n", - "hallucination_grader = hallucination_prompt | structured_llm_grader\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -326,36 +197,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeAnswer(BaseModel):\n", - " \"\"\"Binary score to assess answer addresses question.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer addresses the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n", - " Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n", - "answer_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n", - " ]\n", - ")\n", - "\n", - "answer_grader = answer_prompt | structured_llm_grader\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "code", @@ -374,28 +216,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Question Re-writer\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "\n", - "# Prompt\n", - "system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", - " for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n", - "re_write_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\"system\", system),\n", - " (\n", - " \"human\",\n", - " \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n", - " ),\n", - " ]\n", - ")\n", - "\n", - "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", - "question_rewriter.invoke({\"question\": question})" - ] + "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] }, { "cell_type": "markdown", @@ -415,26 +236,7 @@ "id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] }, { "cell_type": "code", @@ -442,167 +244,7 @@ "id": "add509d8-6682-4127-8d95-13dd37d79702", "metadata": {}, "outputs": [], - "source": [ - "### Nodes\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.get_relevant_documents(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question}\n", - "\n", - "\n", - "def transform_query(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates question key with a re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Re-write question\n", - " better_question = question_rewriter.invoke({\"question\": question})\n", - " return {\"documents\": documents, \"question\": better_question}\n", - "\n", - "\n", - "### Edges\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " filtered_documents = state[\"documents\"]\n", - "\n", - " if not filtered_documents:\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", - " )\n", - " return \"transform_query\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score.binary_score\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"" - ] + "source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] }, { "cell_type": "markdown", @@ -620,42 +262,7 @@ "id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", - "\n", - "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"transform_query\": \"transform_query\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"transform_query\", \"retrieve\")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\",\n", - " \"useful\": END,\n", - " \"not useful\": \"transform_query\",\n", - " },\n", - ")\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "code", @@ -694,22 +301,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Explain how the different types of agent memory work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "code", @@ -749,19 +341,7 @@ ] } ], - "source": [ - "inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -781,7 +361,7 @@ "id": "42369ab8-322d-434a-b5dd-2266e4cb2903", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_self_rag_local.ipynb b/examples/rag/langgraph_self_rag_local.ipynb index 2d0af6f93..a3e307d05 100644 --- a/examples/rag/langgraph_self_rag_local.ipynb +++ b/examples/rag/langgraph_self_rag_local.ipynb @@ -59,10 +59,7 @@ "id": "d7f9cc6d-a70c-433a-b0ad-ea47c5a0717e", "metadata": {}, "outputs": [], - "source": [ - "%capture --no-stderr\n", - "%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]" - ] + "source": ["%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]"] }, { "cell_type": "markdown", @@ -94,10 +91,7 @@ "id": "bedffc73-6b10-42c8-8768-2085c8ed3398", "metadata": {}, "outputs": [], - "source": [ - "# Ollama model name\n", - "local_llm = \"mistral\"" - ] + "source": ["# Ollama model name\nlocal_llm = \"mistral\""] }, { "cell_type": "markdown", @@ -115,13 +109,7 @@ "id": "2208f342-8163-4af3-8dc0-aa70f5e06143", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "markdown", @@ -139,34 +127,7 @@ "id": "c3bb9060-ad74-4470-9991-2ba167b6b8d8", "metadata": {}, "outputs": [], - "source": [ - "from langchain.text_splitter import RecursiveCharacterTextSplitter\n", - "from langchain_community.document_loaders import WebBaseLoader\n", - "from langchain_community.vectorstores import Chroma\n", - "from langchain_nomic.embeddings import NomicEmbeddings\n", - "\n", - "urls = [\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", - " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", - " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", - "]\n", - "\n", - "docs = [WebBaseLoader(url).load() for url in urls]\n", - "docs_list = [item for sublist in docs for item in sublist]\n", - "\n", - "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", - " chunk_size=250, chunk_overlap=0\n", - ")\n", - "doc_splits = text_splitter.split_documents(docs_list)\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = Chroma.from_documents(\n", - " documents=doc_splits,\n", - " collection_name=\"rag-chroma\",\n", - " embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "markdown", @@ -190,33 +151,7 @@ ] } ], - "source": [ - "### Retrieval Grader\n", - "\n", - "from langchain.prompts import PromptTemplate\n", - "from langchain_community.chat_models import ChatOllama\n", - "from langchain_core.output_parsers import JsonOutputParser\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", - " Here is the retrieved document: \\n\\n {document} \\n\\n\n", - " Here is the user question: {question} \\n\n", - " If the document contains keywords related to the user question, grade it as relevant. \\n\n", - " It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n", - " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n", - " input_variables=[\"question\", \"document\"],\n", - ")\n", - "\n", - "retrieval_grader = prompt | llm | JsonOutputParser()\n", - "question = \"agent memory\"\n", - "docs = retriever.get_relevant_documents(question)\n", - "doc_txt = docs[1].page_content\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "code", @@ -232,31 +167,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain import hub\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, temperature=0)\n", - "\n", - "\n", - "# Post-processing\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", - "\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -275,28 +186,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n", - " Here are the facts:\n", - " \\n ------- \\n\n", - " {documents} \n", - " \\n ------- \\n\n", - " Here is the answer: {generation}\n", - " Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", - " input_variables=[\"generation\", \"documents\"],\n", - ")\n", - "\n", - "hallucination_grader = prompt | llm | JsonOutputParser()\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -315,28 +205,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n", - "\n", - "# Prompt\n", - "prompt = PromptTemplate(\n", - " template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n", - " Here is the answer:\n", - " \\n ------- \\n\n", - " {generation} \n", - " \\n ------- \\n\n", - " Here is the question: {question}\n", - " Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n", - " Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n", - " input_variables=[\"generation\", \"question\"],\n", - ")\n", - "\n", - "answer_grader = prompt | llm | JsonOutputParser()\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "code", @@ -355,23 +224,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Question Re-writer\n", - "\n", - "# LLM\n", - "llm = ChatOllama(model=local_llm, temperature=0)\n", - "\n", - "# Prompt\n", - "re_write_prompt = PromptTemplate(\n", - " template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n", - " for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n", - " Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n", - " input_variables=[\"generation\", \"question\"],\n", - ")\n", - "\n", - "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", - "question_rewriter.invoke({\"question\": question})" - ] + "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"] }, { "cell_type": "markdown", @@ -391,26 +244,7 @@ "id": "90fb1dc6-c482-483a-8441-39965c401beb", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] }, { "cell_type": "code", @@ -418,167 +252,7 @@ "id": "5324ea49-5745-47b5-a0a5-bf58c8babe46", "metadata": {}, "outputs": [], - "source": [ - "### Nodes\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.get_relevant_documents(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score[\"score\"]\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question}\n", - "\n", - "\n", - "def transform_query(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates question key with a re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Re-write question\n", - " better_question = question_rewriter.invoke({\"question\": question})\n", - " return {\"documents\": documents, \"question\": better_question}\n", - "\n", - "\n", - "### Edges\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " filtered_documents = state[\"documents\"]\n", - "\n", - " if not filtered_documents:\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", - " )\n", - " return \"transform_query\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score[\"score\"]\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score[\"score\"]\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"" - ] + "source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] }, { "cell_type": "markdown", @@ -596,42 +270,7 @@ "id": "5605dee4-b2df-46ae-a640-cc2ed90c21a6", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", - "\n", - "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"transform_query\": \"transform_query\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"transform_query\", \"retrieve\")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\",\n", - " \"useful\": END,\n", - " \"not useful\": \"transform_query\",\n", - " },\n", - ")\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "markdown", @@ -685,22 +324,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " # Optional: print full state at each node\n", - " # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Explain how the different types of agent memory work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "markdown", @@ -718,7 +342,7 @@ "id": "953143c2-2f2a-4361-a36b-87db7cf21d63", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rag/langgraph_self_rag_pinecone_movies.ipynb b/examples/rag/langgraph_self_rag_pinecone_movies.ipynb index a21c95c5c..b466aadde 100644 --- a/examples/rag/langgraph_self_rag_pinecone_movies.ipynb +++ b/examples/rag/langgraph_self_rag_pinecone_movies.ipynb @@ -33,9 +33,7 @@ "id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9", "metadata": {}, "outputs": [], - "source": [ - "%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph" - ] + "source": ["%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"] }, { "cell_type": "markdown", @@ -53,13 +51,7 @@ "id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n", - "os.environ[\"LANGCHAIN_API_KEY\"] = \"\"" - ] + "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""] }, { "cell_type": "code", @@ -67,11 +59,7 @@ "id": "88637820", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\"" - ] + "source": ["import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""] }, { "cell_type": "markdown", @@ -89,20 +77,7 @@ "id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_pinecone import PineconeVectorStore\n", - "\n", - "# use pinecone movies database\n", - "\n", - "# Add to vectorDB\n", - "vectorstore = PineconeVectorStore(\n", - " embedding=OpenAIEmbeddings(),\n", - " index_name=\"sample-movies\",\n", - " text_key=\"summary\",\n", - ")\n", - "retriever = vectorstore.as_retriever()" - ] + "source": ["from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"] }, { "cell_type": "code", @@ -129,13 +104,7 @@ ] } ], - "source": [ - "docs = retriever.invoke(\"James Cameron\")\n", - "for doc in docs:\n", - " print(\"# \" + doc.metadata[\"title\"])\n", - " print(doc.page_content)\n", - " print()" - ] + "source": ["docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"] }, { "cell_type": "markdown", @@ -151,32 +120,7 @@ "id": "1fafad21-60cc-483e-92a3-6a7edb1838e3", "metadata": {}, "outputs": [], - "source": [ - "### Retrieval Grader\n", - "\n", - "from langchain import hub\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "\n", - "# Data model\n", - "class GradeDocuments(BaseModel):\n", - " \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Documents are relevant to the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\n", - "grade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeDocuments)\n", - "\n", - "retrieval_grader = grade_prompt | structured_llm_grader" - ] + "source": ["### Retrieval Grader\n\nfrom langchain import hub\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\ngrade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\nretrieval_grader = grade_prompt | structured_llm_grader"] }, { "cell_type": "code", @@ -193,14 +137,7 @@ ] } ], - "source": [ - "# Test the retrieval grader\n", - "question = \"movies starring jason momoa\"\n", - "docs = retriever.invoke(question)\n", - "doc_txt = docs[0].page_content\n", - "print(doc_txt)\n", - "print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))" - ] + "source": ["# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"] }, { "cell_type": "markdown", @@ -226,25 +163,7 @@ ] } ], - "source": [ - "### Generate\n", - "\n", - "from langchain import hub\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "# Prompt\n", - "prompt = hub.pull(\"rlm/rag-prompt\")\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", - "\n", - "# Chain\n", - "rag_chain = prompt | llm | StrOutputParser()\n", - "\n", - "# Run\n", - "generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n", - "print(generation)" - ] + "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"] }, { "cell_type": "code", @@ -270,30 +189,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Hallucination Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeHallucinations(BaseModel):\n", - " \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n", - "\n", - "# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\n", - "hallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n", - "\n", - "hallucination_grader = hallucination_prompt | structured_llm_grader\n", - "print(generation)\n", - "hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})" - ] + "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\nhallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nprint(generation)\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"] }, { "cell_type": "code", @@ -320,31 +216,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Answer Grader\n", - "\n", - "\n", - "# Data model\n", - "class GradeAnswer(BaseModel):\n", - " \"\"\"Binary score to assess answer addresses question.\"\"\"\n", - "\n", - " binary_score: str = Field(\n", - " description=\"Answer addresses the question, 'yes' or 'no'\"\n", - " )\n", - "\n", - "\n", - "# LLM with function call\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "structured_llm_grader = llm.with_structured_output(GradeAnswer)\n", - "\n", - "# Prompt\n", - "answer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n", - "\n", - "answer_grader = answer_prompt | structured_llm_grader\n", - "print(question)\n", - "print(generation)\n", - "answer_grader.invoke({\"question\": question, \"generation\": generation})" - ] + "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nanswer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n\nanswer_grader = answer_prompt | structured_llm_grader\nprint(question)\nprint(generation)\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"] }, { "cell_type": "code", @@ -370,19 +242,7 @@ "output_type": "execute_result" } ], - "source": [ - "### Question Re-writer\n", - "\n", - "# LLM\n", - "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n", - "\n", - "# Prompt\n", - "re_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n", - "\n", - "question_rewriter = re_write_prompt | llm | StrOutputParser()\n", - "print(question)\n", - "question_rewriter.invoke({\"question\": question})" - ] + "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"] }, { "cell_type": "markdown", @@ -402,26 +262,7 @@ "id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085", "metadata": {}, "outputs": [], - "source": [ - "from typing import List\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "class GraphState(TypedDict):\n", - " \"\"\"\n", - " Represents the state of our graph.\n", - "\n", - " Attributes:\n", - " question: question\n", - " generation: LLM generation\n", - " documents: list of documents\n", - " \"\"\"\n", - "\n", - " question: str\n", - " generation: str\n", - " documents: List[str]" - ] + "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"] }, { "cell_type": "code", @@ -429,97 +270,7 @@ "id": "add509d8-6682-4127-8d95-13dd37d79702", "metadata": {}, "outputs": [], - "source": [ - "### Nodes\n", - "\n", - "\n", - "def retrieve(state):\n", - " \"\"\"\n", - " Retrieve documents\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, documents, that contains retrieved documents\n", - " \"\"\"\n", - " print(\"---RETRIEVE---\")\n", - " question = state[\"question\"]\n", - "\n", - " # Retrieval\n", - " documents = retriever.invoke(question)\n", - " return {\"documents\": documents, \"question\": question}\n", - "\n", - "\n", - "def generate(state):\n", - " \"\"\"\n", - " Generate answer\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): New key added to state, generation, that contains LLM generation\n", - " \"\"\"\n", - " print(\"---GENERATE---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # RAG generation\n", - " generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n", - " return {\"documents\": documents, \"question\": question, \"generation\": generation}\n", - "\n", - "\n", - "def grade_documents(state):\n", - " \"\"\"\n", - " Determines whether the retrieved documents are relevant to the question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates documents key with only filtered relevant documents\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Score each doc\n", - " filtered_docs = []\n", - " for d in documents:\n", - " score = retrieval_grader.invoke(\n", - " {\"question\": question, \"document\": d.page_content}\n", - " )\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---GRADE: DOCUMENT RELEVANT---\")\n", - " filtered_docs.append(d)\n", - " else:\n", - " print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n", - " continue\n", - " return {\"documents\": filtered_docs, \"question\": question}\n", - "\n", - "\n", - "def transform_query(state):\n", - " \"\"\"\n", - " Transform the query to produce a better question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " state (dict): Updates question key with a re-phrased question\n", - " \"\"\"\n", - "\n", - " print(\"---TRANSFORM QUERY---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - "\n", - " # Re-write question\n", - " better_question = question_rewriter.invoke({\"question\": question})\n", - " return {\"documents\": documents, \"question\": better_question}" - ] + "source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}"] }, { "cell_type": "code", @@ -527,76 +278,7 @@ "id": "09fc91b4", "metadata": {}, "outputs": [], - "source": [ - "### Edges\n", - "\n", - "\n", - "def decide_to_generate(state):\n", - " \"\"\"\n", - " Determines whether to generate an answer, or re-generate a question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Binary decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---ASSESS GRADED DOCUMENTS---\")\n", - " state[\"question\"]\n", - " filtered_documents = state[\"documents\"]\n", - "\n", - " if not filtered_documents:\n", - " # All documents have been filtered check_relevance\n", - " # We will re-generate a new query\n", - " print(\n", - " \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n", - " )\n", - " return \"transform_query\"\n", - " else:\n", - " # We have relevant documents, so generate answer\n", - " print(\"---DECISION: GENERATE---\")\n", - " return \"generate\"\n", - "\n", - "\n", - "def grade_generation_v_documents_and_question(state):\n", - " \"\"\"\n", - " Determines whether the generation is grounded in the document and answers question.\n", - "\n", - " Args:\n", - " state (dict): The current graph state\n", - "\n", - " Returns:\n", - " str: Decision for next node to call\n", - " \"\"\"\n", - "\n", - " print(\"---CHECK HALLUCINATIONS---\")\n", - " question = state[\"question\"]\n", - " documents = state[\"documents\"]\n", - " generation = state[\"generation\"]\n", - "\n", - " score = hallucination_grader.invoke(\n", - " {\"documents\": documents, \"generation\": generation}\n", - " )\n", - " grade = score.binary_score\n", - "\n", - " # Check hallucination\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n", - " # Check question-answering\n", - " print(\"---GRADE GENERATION vs QUESTION---\")\n", - " score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n", - " grade = score.binary_score\n", - " if grade == \"yes\":\n", - " print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n", - " return \"useful\"\n", - " else:\n", - " print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n", - " return \"not useful\"\n", - " else:\n", - " pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n", - " return \"not supported\"" - ] + "source": ["### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""] }, { "cell_type": "markdown", @@ -614,42 +296,7 @@ "id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "workflow = StateGraph(GraphState)\n", - "\n", - "# Define the nodes\n", - "workflow.add_node(\"retrieve\", retrieve) # retrieve\n", - "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n", - "workflow.add_node(\"generate\", generate) # generatae\n", - "workflow.add_node(\"transform_query\", transform_query) # transform_query\n", - "\n", - "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", - "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", - "workflow.add_conditional_edges(\n", - " \"grade_documents\",\n", - " decide_to_generate,\n", - " {\n", - " \"transform_query\": \"transform_query\",\n", - " \"generate\": \"generate\",\n", - " },\n", - ")\n", - "workflow.add_edge(\"transform_query\", \"retrieve\")\n", - "workflow.add_conditional_edges(\n", - " \"generate\",\n", - " grade_generation_v_documents_and_question,\n", - " {\n", - " \"not supported\": \"generate\",\n", - " \"useful\": END,\n", - " \"not useful\": \"transform_query\",\n", - " },\n", - ")\n", - "\n", - "# Compile\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"] }, { "cell_type": "code", @@ -684,20 +331,7 @@ ] } ], - "source": [ - "from pprint import pprint\n", - "\n", - "# Run\n", - "inputs = {\"question\": \"Movies that star Daniel Craig\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "code", @@ -705,17 +339,7 @@ "id": "4138bc51-8c84-4b8a-8d24-f7f470721f6f", "metadata": {}, "outputs": [], - "source": [ - "inputs = {\"question\": \"Which movies are about aliens?\"}\n", - "for output in app.stream(inputs):\n", - " for key, value in output.items():\n", - " # Node\n", - " pprint(f\"Node '{key}':\")\n", - " pprint(\"\\n---\\n\")\n", - "\n", - "# Final generation\n", - "pprint(value[\"generation\"])" - ] + "source": ["inputs = {\"question\": \"Which movies are about aliens?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"] }, { "cell_type": "code", @@ -723,7 +347,7 @@ "id": "42369ab8-322d-434a-b5dd-2266e4cb2903", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 5094e977e..72f3d1bc3 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -32,10 +32,7 @@ "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", @@ -43,24 +40,7 @@ "id": "3368f330-cad6-4d35-a291-68fbf4389d98", "metadata": {}, "outputs": [], - "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\")" - ] + "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\")"] }, { "cell_type": "markdown", @@ -78,28 +58,7 @@ "id": "cc10028f-9cef-4936-9419-cbdf06d24f1e", "metadata": {}, "outputs": [], - "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" - ] + "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"] }, { "cell_type": "code", @@ -127,15 +86,7 @@ ] } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -151,19 +102,7 @@ "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", - ")\n", - "reflect = 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)\nreflect = reflection_prompt | llm"] }, { "cell_type": "code", @@ -193,12 +132,7 @@ ] } ], - "source": [ - "reflection = \"\"\n", - "for chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n", - " print(chunk.content, end=\"\")\n", - " reflection += chunk.content" - ] + "source": ["reflection = \"\"\nfor chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n print(chunk.content, end=\"\")\n reflection += chunk.content"] }, { "cell_type": "markdown", @@ -236,12 +170,7 @@ ] } ], - "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", @@ -259,45 +188,7 @@ "id": "9e9a9d7c-5d2e-4194-b745-4511ec20db76", "metadata": {}, "outputs": [], - "source": [ - "from typing import List, Sequence\n", - "\n", - "from langgraph.graph import END, MessageGraph\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 = MessageGraph()\n", - "builder.add_node(\"generate\", generation_node)\n", - "builder.add_node(\"reflect\", reflection_node)\n", - "builder.set_entry_point(\"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()" - ] + "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()"] }, { "cell_type": "code", @@ -328,17 +219,7 @@ ] } ], - "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", @@ -490,9 +371,7 @@ ] } ], - "source": [ - "ChatPromptTemplate.from_messages(event[END]).pretty_print()" - ] + "source": ["ChatPromptTemplate.from_messages(event[END]).pretty_print()"] }, { "cell_type": "markdown", @@ -510,7 +389,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 ec4d03bd3..9dcbcc7c7 100644 --- a/examples/reflexion/reflexion.ipynb +++ b/examples/reflexion/reflexion.ipynb @@ -40,10 +40,7 @@ "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", @@ -51,25 +48,7 @@ "id": "a917bb70-f84c-48e6-8d32-d14f9df2ca2f", "metadata": {}, "outputs": [], - "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\")" - ] + "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\")"] }, { "cell_type": "code", @@ -77,15 +56,7 @@ "id": "567b6c4a", "metadata": {}, "outputs": [], - "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\")" - ] + "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\")"] }, { "cell_type": "markdown", @@ -110,13 +81,7 @@ "id": "5a2ac853-b8a6-40de-b7fe-3f9f3c5ca4d2", "metadata": {}, "outputs": [], - "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)" - ] + "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)"] }, { "cell_type": "markdown", @@ -132,54 +97,7 @@ "id": "5fffa8d5-068a-4f0b-adfc-b4daf30ef294", "metadata": {}, "outputs": [], - "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" - ] + "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"] }, { "cell_type": "code", @@ -196,40 +114,7 @@ ] } ], - "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", - ")" - ] + "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)"] }, { "cell_type": "code", @@ -237,10 +122,7 @@ "id": "5922e1fe-7533-4f41-8b1d-d812707c1968", "metadata": {}, "outputs": [], - "source": [ - "example_question = \"Why is reflection useful in AI?\"\n", - "initial = first_responder.respond([HumanMessage(content=example_question)])" - ] + "source": ["example_question = \"Why is reflection useful in AI?\"\ninitial = first_responder.respond([HumanMessage(content=example_question)])"] }, { "cell_type": "markdown", @@ -258,38 +140,7 @@ "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\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)" - ] + "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)"] }, { "cell_type": "code", @@ -308,25 +159,7 @@ "output_type": "execute_result" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -344,24 +177,7 @@ "id": "fccd6a17", "metadata": {}, "outputs": [], - "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", - ")" - ] + "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)"] }, { "cell_type": "markdown", @@ -380,48 +196,7 @@ "id": "3c57318f-a30c-4dbd-9b88-f2633e8cb3b1", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "from langgraph.graph import END, MessageGraph\n", - "\n", - "MAX_ITERATIONS = 5\n", - "builder = MessageGraph()\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.set_entry_point(\"draft\")\n", - "graph = builder.compile()" - ] + "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()"] }, { "cell_type": "code", @@ -440,15 +215,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "code", @@ -563,15 +330,7 @@ ] } ], - "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()" - ] + "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()"] }, { "cell_type": "markdown", diff --git a/examples/respond-in-format.ipynb b/examples/respond-in-format.ipynb index a77a30791..6ac0a9d87 100644 --- a/examples/respond-in-format.ipynb +++ b/examples/respond-in-format.ipynb @@ -30,10 +30,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain-anthropic" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-anthropic"] }, { "cell_type": "markdown", @@ -49,18 +46,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -76,10 +62,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -105,22 +88,7 @@ "id": "c9172aa0", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# Add messages essentially does this with more\n", - "# robust handling\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -140,19 +108,7 @@ "id": "3a1c8796", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder, but don't tell the LLM that...\n", - " return [\"The weather will be sunny with a high of 27 C.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The weather will be sunny with a high of 27 C.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -169,11 +125,7 @@ "id": "56681368", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -197,11 +149,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -224,20 +172,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class Response(BaseModel):\n", - " \"\"\"Final response to the user\"\"\"\n", - "\n", - " temperature: float = Field(description=\"the temperature\")\n", - " other_notes: str = Field(description=\"any other notes about the weather\")\n", - "\n", - "\n", - "# Bind to the actual tools + the response format!\n", - "model = model.bind_tools(tools + [Response], tool_choice=\"any\")" - ] + "source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Response(BaseModel):\n \"\"\"Final response to the user\"\"\"\n\n temperature: float = Field(description=\"the temperature\")\n other_notes: str = Field(description=\"any other notes about the weather\")\n\n\n# Bind to the actual tools + the response format!\nmodel = model.bind_tools(tools + [Response], tool_choice=\"any\")"] }, { "cell_type": "markdown", @@ -263,16 +198,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence, TypedDict\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "\n", - "\n", - "class AgentState(TypedDict):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -311,31 +237,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def route(state: AgentState) -> Literal[\"action\", \"__end__\"]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"__end__\"\n", - " # Otherwise if there is, we need to check what type of function call it is\n", - " if last_message.tool_calls[0][\"name\"] == Response.__name__:\n", - " return \"__end__\"\n", - " # Otherwise we continue\n", - " return \"action\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state: AgentState):\n", - " messages = state[\"messages\"]\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}" - ] + "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef route(state: AgentState) -> Literal[\"action\", \"__end__\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"__end__\"\n # Otherwise if there is, we need to check what type of function call it is\n if last_message.tool_calls[0][\"name\"] == Response.__name__:\n return \"__end__\"\n # Otherwise we continue\n return \"action\"\n\n\n# Define the function that calls the model\ndef call_model(state: AgentState):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"] }, { "cell_type": "markdown", @@ -353,38 +255,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " route,\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n route,\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -403,11 +274,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph(xray=True).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -465,15 +332,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for output in app.stream(inputs, stream_mode=\"values\"):\n", - " last_msg = output[\"messages\"][-1]\n", - " last_msg.pretty_print()\n", - " print(\"\\n---\\n\")" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs, stream_mode=\"values\"):\n last_msg = output[\"messages\"][-1]\n last_msg.pretty_print()\n print(\"\\n---\\n\")"] }, { "cell_type": "code", @@ -481,7 +340,7 @@ "id": "eed4360d-2cdf-497b-b03f-8bc51062f780", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/rewoo/rewoo.ipynb b/examples/rewoo/rewoo.ipynb index a9cd18569..a98d34388 100644 --- a/examples/rewoo/rewoo.ipynb +++ b/examples/rewoo/rewoo.ipynb @@ -47,9 +47,7 @@ "id": "7f52bded-9d23-4826-8bfc-20b0d3a51182", "metadata": {}, "outputs": [], - "source": [ - "# %pip install -U langgraph langchain_community langchain_openai tavily-python" - ] + "source": ["# %pip install -U langgraph langchain_community langchain_openai tavily-python"] }, { "cell_type": "code", @@ -57,22 +55,7 @@ "id": "4215f9fb-71ff-4d88-8484-f73174db5592", "metadata": {}, "outputs": [], - "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\"{var}=\")\n", - "\n", - "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"ReWOO\"\n", - "_set_if_undefined(\"TAVILY_API_KEY\")\n", - "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", - "_set_if_undefined(\"OPENAI_API_KEY\")" - ] + "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\"{var}=\")\n\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"ReWOO\"\n_set_if_undefined(\"TAVILY_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -90,17 +73,7 @@ "id": "9a92c875-c20b-4b7e-9d88-61c62382f8e2", "metadata": {}, "outputs": [], - "source": [ - "from typing import List, TypedDict\n", - "\n", - "\n", - "class ReWOO(TypedDict):\n", - " task: str\n", - " plan_string: str\n", - " steps: List\n", - " results: dict\n", - " result: str" - ] + "source": ["from typing import List, TypedDict\n\n\nclass ReWOO(TypedDict):\n task: str\n plan_string: str\n steps: List\n results: dict\n result: str"] }, { "cell_type": "markdown", @@ -127,11 +100,7 @@ "id": "c8836921-c89e-42b6-8c71-27aeaeac5368", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "code", @@ -139,32 +108,7 @@ "id": "7e7faa92-30a1-4942-b3c7-acd3a7bfccbc", "metadata": {}, "outputs": [], - "source": [ - "prompt = \"\"\"For the following task, make plans that can solve the problem step by step. For each plan, indicate \\\n", - "which external tool together with tool input to retrieve evidence. You can store the evidence into a \\\n", - "variable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)\n", - "\n", - "Tools can be one of the following:\n", - "(1) Google[input]: Worker that searches results from Google. Useful when you need to find short\n", - "and succinct answers about a specific topic. The input should be a search query.\n", - "(2) LLM[input]: A pretrained LLM like yourself. Useful when you need to act with general\n", - "world knowledge and common sense. Prioritize it when you are confident in solving the problem\n", - "yourself. Input can be any instruction.\n", - "\n", - "For example,\n", - "Task: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x\n", - "hours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours\n", - "less than Toby. How many hours did Rebecca work?\n", - "Plan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve\n", - "with Wolfram Alpha. #E1 = WolframAlpha[Solve x + (2x − 10) + ((2x − 10) − 8) = 157]\n", - "Plan: Find out the number of hours Thomas worked. #E2 = LLM[What is x, given #E1]\n", - "Plan: Calculate the number of hours Rebecca worked. #E3 = Calculator[(2 ∗ #E2 − 10) − 8]\n", - "\n", - "Begin! \n", - "Describe your plans with rich details. Each Plan should be followed by only one #E.\n", - "\n", - "Task: {task}\"\"\"" - ] + "source": ["prompt = \"\"\"For the following task, make plans that can solve the problem step by step. For each plan, indicate \\\nwhich external tool together with tool input to retrieve evidence. You can store the evidence into a \\\nvariable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)\n\nTools can be one of the following:\n(1) Google[input]: Worker that searches results from Google. Useful when you need to find short\nand succinct answers about a specific topic. The input should be a search query.\n(2) LLM[input]: A pretrained LLM like yourself. Useful when you need to act with general\nworld knowledge and common sense. Prioritize it when you are confident in solving the problem\nyourself. Input can be any instruction.\n\nFor example,\nTask: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x\nhours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours\nless than Toby. How many hours did Rebecca work?\nPlan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve\nwith Wolfram Alpha. #E1 = WolframAlpha[Solve x + (2x − 10) + ((2x − 10) − 8) = 157]\nPlan: Find out the number of hours Thomas worked. #E2 = LLM[What is x, given #E1]\nPlan: Calculate the number of hours Rebecca worked. #E3 = Calculator[(2 ∗ #E2 − 10) − 8]\n\nBegin! \nDescribe your plans with rich details. Each Plan should be followed by only one #E.\n\nTask: {task}\"\"\""] }, { "cell_type": "code", @@ -172,9 +116,7 @@ "id": "72b4ab0f-7215-4f4b-9407-0ebad8b13b92", "metadata": {}, "outputs": [], - "source": [ - "task = \"what is the hometown of the 2024 australian open winner\"" - ] + "source": ["task = \"what is the hometown of the 2024 australian open winner\""] }, { "cell_type": "code", @@ -182,9 +124,7 @@ "id": "56ecb45b-ea76-4303-a4f3-51406fe8312a", "metadata": {}, "outputs": [], - "source": [ - "result = model.invoke(prompt.format(task=task))" - ] + "source": ["result = model.invoke(prompt.format(task=task))"] }, { "cell_type": "code", @@ -210,9 +150,7 @@ ] } ], - "source": [ - "print(result.content)" - ] + "source": ["print(result.content)"] }, { "cell_type": "markdown", @@ -231,24 +169,7 @@ "id": "f9f042b6-90d8-430f-abf3-04ad2bb047c7", "metadata": {}, "outputs": [], - "source": [ - "import re\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "\n", - "# Regex to match expressions of the form E#... = ...[...]\n", - "regex_pattern = r\"Plan:\\s*(.+)\\s*(#E\\d+)\\s*=\\s*(\\w+)\\s*\\[([^\\]]+)\\]\"\n", - "prompt_template = ChatPromptTemplate.from_messages([(\"user\", prompt)])\n", - "planner = prompt_template | model\n", - "\n", - "\n", - "def get_plan(state: ReWOO):\n", - " task = state[\"task\"]\n", - " result = planner.invoke({\"task\": task})\n", - " # Find all matches in the sample text\n", - " matches = re.findall(regex_pattern, result.content)\n", - " return {\"steps\": matches, \"plan_string\": result.content}" - ] + "source": ["import re\n\nfrom langchain_core.prompts import ChatPromptTemplate\n\n# Regex to match expressions of the form E#... = ...[...]\nregex_pattern = r\"Plan:\\s*(.+)\\s*(#E\\d+)\\s*=\\s*(\\w+)\\s*\\[([^\\]]+)\\]\"\nprompt_template = ChatPromptTemplate.from_messages([(\"user\", prompt)])\nplanner = prompt_template | model\n\n\ndef get_plan(state: ReWOO):\n task = state[\"task\"]\n result = planner.invoke({\"task\": task})\n # Find all matches in the sample text\n matches = re.findall(regex_pattern, result.content)\n return {\"steps\": matches, \"plan_string\": result.content}"] }, { "cell_type": "markdown", @@ -268,11 +189,7 @@ "id": "3412cfc4-6796-4295-aea4-7eeb304e10bd", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "\n", - "search = TavilySearchResults()" - ] + "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\nsearch = TavilySearchResults()"] }, { "cell_type": "code", @@ -280,32 +197,7 @@ "id": "aa96fbac-28bc-4afe-ae35-ddb3383d1147", "metadata": {}, "outputs": [], - "source": [ - "def _get_current_task(state: ReWOO):\n", - " if state[\"results\"] is None:\n", - " return 1\n", - " if len(state[\"results\"]) == len(state[\"steps\"]):\n", - " return None\n", - " else:\n", - " return len(state[\"results\"]) + 1\n", - "\n", - "\n", - "def tool_execution(state: ReWOO):\n", - " \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n", - " _step = _get_current_task(state)\n", - " _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n", - " _results = state[\"results\"] or {}\n", - " for k, v in _results.items():\n", - " tool_input = tool_input.replace(k, v)\n", - " if tool == \"Google\":\n", - " result = search.invoke(tool_input)\n", - " elif tool == \"LLM\":\n", - " result = model.invoke(tool_input)\n", - " else:\n", - " raise ValueError\n", - " _results[step_name] = str(result)\n", - " return {\"results\": _results}" - ] + "source": ["def _get_current_task(state: ReWOO):\n if state[\"results\"] is None:\n return 1\n if len(state[\"results\"]) == len(state[\"steps\"]):\n return None\n else:\n return len(state[\"results\"]) + 1\n\n\ndef tool_execution(state: ReWOO):\n \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n _step = _get_current_task(state)\n _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n _results = state[\"results\"] or {}\n for k, v in _results.items():\n tool_input = tool_input.replace(k, v)\n if tool == \"Google\":\n result = search.invoke(tool_input)\n elif tool == \"LLM\":\n result = model.invoke(tool_input)\n else:\n raise ValueError\n _results[step_name] = str(result)\n return {\"results\": _results}"] }, { "cell_type": "markdown", @@ -323,32 +215,7 @@ "id": "0a4d9851-8590-42be-8c53-9969ebff85f4", "metadata": {}, "outputs": [], - "source": [ - "solve_prompt = \"\"\"Solve the following task or problem. To solve the problem, we have made step-by-step Plan and \\\n", - "retrieved corresponding Evidence to each Plan. Use them with caution since long evidence might \\\n", - "contain irrelevant information.\n", - "\n", - "{plan}\n", - "\n", - "Now solve the question or task according to provided Evidence above. Respond with the answer\n", - "directly with no extra words.\n", - "\n", - "Task: {task}\n", - "Response:\"\"\"\n", - "\n", - "\n", - "def solve(state: ReWOO):\n", - " plan = \"\"\n", - " for _plan, step_name, tool, tool_input in state[\"steps\"]:\n", - " _results = state[\"results\"] or {}\n", - " for k, v in _results.items():\n", - " tool_input = tool_input.replace(k, v)\n", - " step_name = step_name.replace(k, v)\n", - " plan += f\"Plan: {_plan}\\n{step_name} = {tool}[{tool_input}]\"\n", - " prompt = solve_prompt.format(plan=plan, task=state[\"task\"])\n", - " result = model.invoke(prompt)\n", - " return {\"result\": result.content}" - ] + "source": ["solve_prompt = \"\"\"Solve the following task or problem. To solve the problem, we have made step-by-step Plan and \\\nretrieved corresponding Evidence to each Plan. Use them with caution since long evidence might \\\ncontain irrelevant information.\n\n{plan}\n\nNow solve the question or task according to provided Evidence above. Respond with the answer\ndirectly with no extra words.\n\nTask: {task}\nResponse:\"\"\"\n\n\ndef solve(state: ReWOO):\n plan = \"\"\n for _plan, step_name, tool, tool_input in state[\"steps\"]:\n _results = state[\"results\"] or {}\n for k, v in _results.items():\n tool_input = tool_input.replace(k, v)\n step_name = step_name.replace(k, v)\n plan += f\"Plan: {_plan}\\n{step_name} = {tool}[{tool_input}]\"\n prompt = solve_prompt.format(plan=plan, task=state[\"task\"])\n result = model.invoke(prompt)\n return {\"result\": result.content}"] }, { "cell_type": "markdown", @@ -366,16 +233,7 @@ "id": "73b235d7-fa83-4e84-9f2e-2908f16deb26", "metadata": {}, "outputs": [], - "source": [ - "def _route(state):\n", - " _step = _get_current_task(state)\n", - " if _step is None:\n", - " # We have executed all tasks\n", - " return \"solve\"\n", - " else:\n", - " # We are still executing tasks, loop back to the \"tool\" node\n", - " return \"tool\"" - ] + "source": ["def _route(state):\n _step = _get_current_task(state)\n if _step is None:\n # We have executed all tasks\n return \"solve\"\n else:\n # We are still executing tasks, loop back to the \"tool\" node\n return \"tool\""] }, { "cell_type": "code", @@ -383,20 +241,7 @@ "id": "cf173aa1-ce31-4dca-8111-30c91e209652", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "graph = StateGraph(ReWOO)\n", - "graph.add_node(\"plan\", get_plan)\n", - "graph.add_node(\"tool\", tool_execution)\n", - "graph.add_node(\"solve\", solve)\n", - "graph.add_edge(\"plan\", \"tool\")\n", - "graph.add_edge(\"solve\", END)\n", - "graph.add_conditional_edges(\"tool\", _route)\n", - "graph.set_entry_point(\"plan\")\n", - "\n", - "app = graph.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\ngraph = StateGraph(ReWOO)\ngraph.add_node(\"plan\", get_plan)\ngraph.add_node(\"tool\", tool_execution)\ngraph.add_node(\"solve\", solve)\ngraph.add_edge(\"plan\", \"tool\")\ngraph.add_edge(\"solve\", END)\ngraph.add_conditional_edges(\"tool\", _route)\ngraph.add_edge(START, \"plan\")\n\napp = graph.compile()"] }, { "cell_type": "code", @@ -425,11 +270,7 @@ ] } ], - "source": [ - "for s in app.stream({\"task\": task}):\n", - " print(s)\n", - " print(\"---\")" - ] + "source": ["for s in app.stream({\"task\": task}):\n print(s)\n print(\"---\")"] }, { "cell_type": "code", @@ -445,10 +286,7 @@ ] } ], - "source": [ - "# Print out the final result\n", - "print(s[END][\"result\"])" - ] + "source": ["# Print out the final result\nprint(s[END][\"result\"])"] }, { "cell_type": "markdown", diff --git a/examples/state-context-key.ipynb b/examples/state-context-key.ipynb index 02ed87ab4..5a96d2ef0 100644 --- a/examples/state-context-key.ipynb +++ b/examples/state-context-key.ipynb @@ -372,7 +372,7 @@ "metadata": {}, "outputs": [], "source": [ - "from langgraph.graph import END, StateGraph\n", + "from langgraph.graph import END, StateGraph, START\n", "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", @@ -383,7 +383,7 @@ "\n", "# Set the entrypoint as `agent`\n", "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", + "workflow.add_edge(START, \"agent\")\n", "\n", "# We now add a conditional edge\n", "workflow.add_conditional_edges(\n", diff --git a/examples/state-model.ipynb b/examples/state-model.ipynb index e264e269d..afc94f45c 100644 --- a/examples/state-model.ipynb +++ b/examples/state-model.ipynb @@ -32,10 +32,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"] }, { "cell_type": "markdown", @@ -51,18 +48,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -78,10 +64,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -101,20 +84,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " # Don't let the LLM know this though 😊\n", - " return [\"The answer to your question lies within.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -133,11 +103,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolExecutor\n", - "\n", - "tool_executor = ToolExecutor(tools)" - ] + "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"] }, { "cell_type": "markdown", @@ -161,11 +127,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -183,9 +145,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -211,17 +171,7 @@ "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, Sequence\n", - "\n", - "from langchain_core.messages import BaseMessage\n", - "from langchain_core.pydantic_v1 import BaseModel\n", - "\n", - "\n", - "class AgentState(BaseModel):\n", - " messages: Annotated[Sequence[BaseMessage], operator.add]" - ] + "source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom langchain_core.messages import BaseMessage\nfrom langchain_core.pydantic_v1 import BaseModel\n\n\nclass AgentState(BaseModel):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] }, { "cell_type": "markdown", @@ -260,53 +210,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "\n", - "from langgraph.prebuilt import ToolInvocation\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state):\n", - " messages = state.messages\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", - "\n", - "\n", - "# Define the function that calls the model\n", - "def call_model(state):\n", - " messages = state.messages\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state):\n", - " messages = state.messages\n", - " # Based on the continue condition\n", - " # we know the last message involves a function call\n", - " last_message = messages[-1]\n", - " # We construct an ToolInvocation from the function_call\n", - " tool_call = last_message.tool_calls[0]\n", - " action = ToolInvocation(\n", - " tool=tool_call[\"name\"],\n", - " tool_input=tool_call[\"args\"],\n", - " )\n", - " # We call the tool_executor and get back a response\n", - " response = tool_executor.invoke(action)\n", - " # We use the response to create a ToolMessage\n", - " tool_message = ToolMessage(\n", - " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", - " )\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state.messages\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state.messages\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state.messages\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -324,50 +228,7 @@ "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState)\n", - "\n", - "# Define the two nodes we will cycle between\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")\n", - "\n", - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] }, { "cell_type": "code", @@ -386,11 +247,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "display(Image(app.get_graph().draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -432,13 +289,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "for chunk in app.stream(inputs, stream_mode=\"values\"):\n", - " chunk[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor chunk in app.stream(inputs, stream_mode=\"values\"):\n chunk[\"messages\"][-1].pretty_print()"] }, { "cell_type": "code", @@ -446,7 +297,7 @@ "id": "296c7456-da05-4326-95dc-47d6b312da9d", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/storm/storm.ipynb b/examples/storm/storm.ipynb index f27649bfa..27a05804f 100644 --- a/examples/storm/storm.ipynb +++ b/examples/storm/storm.ipynb @@ -47,47 +47,21 @@ "execution_count": 1, "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n", - "# We use one or the other search engine below\n", - "%pip install -U duckduckgo tavily-python" - ] + "source": ["%%capture --no-stderr\n%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n# We use one or the other search engine below\n%pip install -U duckduckgo tavily-python"] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], - "source": [ - "# Uncomment if you want to draw the pretty graph diagrams.\n", - "# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n", - "# ! brew install graphviz\n", - "# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" - ] + "source": ["# Uncomment if you want to draw the pretty graph diagrams.\n# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n# ! brew install graphviz\n# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz"] }, { "cell_type": "code", "execution_count": 86, "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if os.environ.get(var):\n", - " return\n", - " os.environ[var] = getpass.getpass(var + \":\")\n", - "\n", - "\n", - "# Set for tracing\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var + \":\")\n\n\n# Set for tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n_set_env(\"LANGCHAIN_API_KEY\")\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -103,14 +77,7 @@ "execution_count": 3, "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "fast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", - "# Uncomment for a Fireworks model\n", - "# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\n", - "long_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nfast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n# Uncomment for a Fireworks model\n# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\nlong_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"] }, { "cell_type": "markdown", @@ -136,66 +103,7 @@ ] } ], - "source": [ - "from typing import List, Optional\n", - "\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n", - " ),\n", - " (\"user\", \"{topic}\"),\n", - " ]\n", - ")\n", - "\n", - "\n", - "class Subsection(BaseModel):\n", - " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", - " description: str = Field(..., title=\"Content of the subsection\")\n", - "\n", - " @property\n", - " def as_str(self) -> str:\n", - " return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n", - "\n", - "\n", - "class Section(BaseModel):\n", - " section_title: str = Field(..., title=\"Title of the section\")\n", - " description: str = Field(..., title=\"Content of the section\")\n", - " subsections: Optional[List[Subsection]] = Field(\n", - " default=None,\n", - " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", - " )\n", - "\n", - " @property\n", - " def as_str(self) -> str:\n", - " subsections = \"\\n\\n\".join(\n", - " f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n", - " for subsection in self.subsections or []\n", - " )\n", - " return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n", - "\n", - "\n", - "class Outline(BaseModel):\n", - " page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n", - " sections: List[Section] = Field(\n", - " default_factory=list,\n", - " title=\"Titles and descriptions for each section of the Wikipedia page.\",\n", - " )\n", - "\n", - " @property\n", - " def as_str(self) -> str:\n", - " sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n", - " return f\"# {self.page_title}\\n\\n{sections}\".strip()\n", - "\n", - "\n", - "generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n", - " Outline\n", - ")" - ] + "source": ["from typing import List, Optional\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\ndirect_gen_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n ),\n (\"user\", \"{topic}\"),\n ]\n)\n\n\nclass Subsection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n description: str = Field(..., title=\"Content of the subsection\")\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n\n\nclass Section(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n description: str = Field(..., title=\"Content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n for subsection in self.subsections or []\n )\n return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n\n\nclass Outline(BaseModel):\n page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n sections: List[Section] = Field(\n default_factory=list,\n title=\"Titles and descriptions for each section of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n return f\"# {self.page_title}\\n\\n{sections}\".strip()\n\n\ngenerate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n Outline\n)"] }, { "cell_type": "code", @@ -226,13 +134,7 @@ ] } ], - "source": [ - "example_topic = \"Impact of million-plus token context window language models on RAG\"\n", - "\n", - "initial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n", - "\n", - "print(initial_outline.as_str)" - ] + "source": ["example_topic = \"Impact of million-plus token context window language models on RAG\"\n\ninitial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n\nprint(initial_outline.as_str)"] }, { "cell_type": "markdown", @@ -250,27 +152,7 @@ "execution_count": 6, "metadata": {}, "outputs": [], - "source": [ - "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n", - " \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n", - "\n", - "Please list the as many subjects and urls as you can.\n", - "\n", - "Topic of interest: {topic}\n", - "\"\"\"\n", - ")\n", - "\n", - "\n", - "class RelatedSubjects(BaseModel):\n", - " topics: List[str] = Field(\n", - " description=\"Comprehensive list of related subjects as background research.\",\n", - " )\n", - "\n", - "\n", - "expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n", - " RelatedSubjects\n", - ")" - ] + "source": ["gen_related_topics_prompt = ChatPromptTemplate.from_template(\n \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n\nPlease list the as many subjects and urls as you can.\n\nTopic of interest: {topic}\n\"\"\"\n)\n\n\nclass RelatedSubjects(BaseModel):\n topics: List[str] = Field(\n description=\"Comprehensive list of related subjects as background research.\",\n )\n\n\nexpand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n RelatedSubjects\n)"] }, { "cell_type": "code", @@ -288,10 +170,7 @@ "output_type": "execute_result" } ], - "source": [ - "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\n", - "related_subjects" - ] + "source": ["related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\nrelated_subjects"] }, { "cell_type": "markdown", @@ -308,99 +187,21 @@ "execution_count": 8, "metadata": {}, "outputs": [], - "source": [ - "class Editor(BaseModel):\n", - " affiliation: str = Field(\n", - " description=\"Primary affiliation of the editor.\",\n", - " )\n", - " name: str = Field(\n", - " description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n", - " )\n", - " role: str = Field(\n", - " description=\"Role of the editor in the context of the topic.\",\n", - " )\n", - " description: str = Field(\n", - " description=\"Description of the editor's focus, concerns, and motives.\",\n", - " )\n", - "\n", - " @property\n", - " def persona(self) -> str:\n", - " return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n", - "\n", - "\n", - "class Perspectives(BaseModel):\n", - " editors: List[Editor] = Field(\n", - " description=\"Comprehensive list of editors with their roles and affiliations.\",\n", - " # Add a pydantic validation/restriction to be at most M editors\n", - " )\n", - "\n", - "\n", - "gen_perspectives_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n", - " You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n", - "\n", - " Wiki page outlines of related topics for inspiration:\n", - " {examples}\"\"\",\n", - " ),\n", - " (\"user\", \"Topic of interest: {topic}\"),\n", - " ]\n", - ")\n", - "\n", - "gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n", - " model=\"gpt-3.5-turbo\"\n", - ").with_structured_output(Perspectives)" - ] + "source": ["class Editor(BaseModel):\n affiliation: str = Field(\n description=\"Primary affiliation of the editor.\",\n )\n name: str = Field(\n description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n )\n role: str = Field(\n description=\"Role of the editor in the context of the topic.\",\n )\n description: str = Field(\n description=\"Description of the editor's focus, concerns, and motives.\",\n )\n\n @property\n def persona(self) -> str:\n return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n\n\nclass Perspectives(BaseModel):\n editors: List[Editor] = Field(\n description=\"Comprehensive list of editors with their roles and affiliations.\",\n # Add a pydantic validation/restriction to be at most M editors\n )\n\n\ngen_perspectives_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n\n Wiki page outlines of related topics for inspiration:\n {examples}\"\"\",\n ),\n (\"user\", \"Topic of interest: {topic}\"),\n ]\n)\n\ngen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Perspectives)"] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.retrievers import WikipediaRetriever\n", - "from langchain_core.runnables import RunnableLambda\n", - "from langchain_core.runnables import chain as as_runnable\n", - "\n", - "wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n", - "\n", - "\n", - "def format_doc(doc, max_length=1000):\n", - " related = \"- \".join(doc.metadata[\"categories\"])\n", - " return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n", - " :max_length\n", - " ]\n", - "\n", - "\n", - "def format_docs(docs):\n", - " return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n", - "\n", - "\n", - "@as_runnable\n", - "async def survey_subjects(topic: str):\n", - " related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n", - " retrieved_docs = await wikipedia_retriever.abatch(\n", - " related_subjects.topics, return_exceptions=True\n", - " )\n", - " all_docs = []\n", - " for docs in retrieved_docs:\n", - " if isinstance(docs, BaseException):\n", - " continue\n", - " all_docs.extend(docs)\n", - " formatted = format_docs(all_docs)\n", - " return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" - ] + "source": ["from langchain_community.retrievers import WikipediaRetriever\nfrom langchain_core.runnables import RunnableLambda\nfrom langchain_core.runnables import chain as as_runnable\n\nwikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n\n\ndef format_doc(doc, max_length=1000):\n related = \"- \".join(doc.metadata[\"categories\"])\n return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n :max_length\n ]\n\n\ndef format_docs(docs):\n return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n\n\n@as_runnable\nasync def survey_subjects(topic: str):\n related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n retrieved_docs = await wikipedia_retriever.abatch(\n related_subjects.topics, return_exceptions=True\n )\n all_docs = []\n for docs in retrieved_docs:\n if isinstance(docs, BaseException):\n continue\n all_docs.extend(docs)\n formatted = format_docs(all_docs)\n return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})"] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], - "source": [ - "perspectives = await survey_subjects.ainvoke(example_topic)" - ] + "source": ["perspectives = await survey_subjects.ainvoke(example_topic)"] }, { "cell_type": "code", @@ -433,9 +234,7 @@ "output_type": "execute_result" } ], - "source": [ - "perspectives.dict()" - ] + "source": ["perspectives.dict()"] }, { "cell_type": "markdown", @@ -456,42 +255,7 @@ "execution_count": 13, "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from langchain_core.messages import AnyMessage\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "\n", - "def add_messages(left, right):\n", - " if not isinstance(left, list):\n", - " left = [left]\n", - " if not isinstance(right, list):\n", - " right = [right]\n", - " return left + right\n", - "\n", - "\n", - "def update_references(references, new_references):\n", - " if not references:\n", - " references = {}\n", - " references.update(new_references)\n", - " return references\n", - "\n", - "\n", - "def update_editor(editor, new_editor):\n", - " # Can only set at the outset\n", - " if not editor:\n", - " return new_editor\n", - " return editor\n", - "\n", - "\n", - "class InterviewState(TypedDict):\n", - " messages: Annotated[List[AnyMessage], add_messages]\n", - " references: Annotated[Optional[dict], update_references]\n", - " editor: Annotated[Optional[Editor], update_editor]" - ] + "source": ["from typing import Annotated\n\nfrom langchain_core.messages import AnyMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef add_messages(left, right):\n if not isinstance(left, list):\n left = [left]\n if not isinstance(right, list):\n right = [right]\n return left + right\n\n\ndef update_references(references, new_references):\n if not references:\n references = {}\n references.update(new_references)\n return references\n\n\ndef update_editor(editor, new_editor):\n # Can only set at the outset\n if not editor:\n return new_editor\n return editor\n\n\nclass InterviewState(TypedDict):\n messages: Annotated[List[AnyMessage], add_messages]\n references: Annotated[Optional[dict], update_references]\n editor: Annotated[Optional[Editor], update_editor]"] }, { "cell_type": "markdown", @@ -507,58 +271,7 @@ "execution_count": 14, "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", - "from langchain_core.prompts import MessagesPlaceholder\n", - "\n", - "gen_qn_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\n", - "Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\n", - "Now, you are chatting with an expert to get information. Ask good questions to get more useful information.\n", - "\n", - "When you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\n", - "Please only ask one question at a time and don't ask what you have asked before.\\\n", - "Your questions should be related to the topic you want to write.\n", - "Be comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n", - "\n", - "Stay true to your specific perspective:\n", - "\n", - "{persona}\"\"\",\n", - " ),\n", - " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", - " ]\n", - ")\n", - "\n", - "\n", - "def tag_with_name(ai_message: AIMessage, name: str):\n", - " ai_message.name = name\n", - " return ai_message\n", - "\n", - "\n", - "def swap_roles(state: InterviewState, name: str):\n", - " converted = []\n", - " for message in state[\"messages\"]:\n", - " if isinstance(message, AIMessage) and message.name != name:\n", - " message = HumanMessage(**message.dict(exclude={\"type\"}))\n", - " converted.append(message)\n", - " return {\"messages\": converted}\n", - "\n", - "\n", - "@as_runnable\n", - "async def generate_question(state: InterviewState):\n", - " editor = state[\"editor\"]\n", - " gn_chain = (\n", - " RunnableLambda(swap_roles).bind(name=editor.name)\n", - " | gen_qn_prompt.partial(persona=editor.persona)\n", - " | fast_llm\n", - " | RunnableLambda(tag_with_name).bind(name=editor.name)\n", - " )\n", - " result = await gn_chain.ainvoke(state)\n", - " return {\"messages\": [result]}" - ] + "source": ["from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\nfrom langchain_core.prompts import MessagesPlaceholder\n\ngen_qn_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\nBesides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\nNow, you are chatting with an expert to get information. Ask good questions to get more useful information.\n\nWhen you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\nPlease only ask one question at a time and don't ask what you have asked before.\\\nYour questions should be related to the topic you want to write.\nBe comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n\nStay true to your specific perspective:\n\n{persona}\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ndef tag_with_name(ai_message: AIMessage, name: str):\n ai_message.name = name\n return ai_message\n\n\ndef swap_roles(state: InterviewState, name: str):\n converted = []\n for message in state[\"messages\"]:\n if isinstance(message, AIMessage) and message.name != name:\n message = HumanMessage(**message.dict(exclude={\"type\"}))\n converted.append(message)\n return {\"messages\": converted}\n\n\n@as_runnable\nasync def generate_question(state: InterviewState):\n editor = state[\"editor\"]\n gn_chain = (\n RunnableLambda(swap_roles).bind(name=editor.name)\n | gen_qn_prompt.partial(persona=editor.persona)\n | fast_llm\n | RunnableLambda(tag_with_name).bind(name=editor.name)\n )\n result = await gn_chain.ainvoke(state)\n return {\"messages\": [result]}"] }, { "cell_type": "code", @@ -576,19 +289,7 @@ "output_type": "execute_result" } ], - "source": [ - "messages = [\n", - " HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n", - "]\n", - "question = await generate_question.ainvoke(\n", - " {\n", - " \"editor\": perspectives.editors[0],\n", - " \"messages\": messages,\n", - " }\n", - ")\n", - "\n", - "question[\"messages\"][0].content" - ] + "source": ["messages = [\n HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n]\nquestion = await generate_question.ainvoke(\n {\n \"editor\": perspectives.editors[0],\n \"messages\": messages,\n }\n)\n\nquestion[\"messages\"][0].content"] }, { "cell_type": "markdown", @@ -604,26 +305,7 @@ "execution_count": 16, "metadata": {}, "outputs": [], - "source": [ - "class Queries(BaseModel):\n", - " queries: List[str] = Field(\n", - " description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n", - " )\n", - "\n", - "\n", - "gen_queries_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n", - " ),\n", - " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", - " ]\n", - ")\n", - "gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n", - " model=\"gpt-3.5-turbo\"\n", - ").with_structured_output(Queries, include_raw=True)" - ] + "source": ["class Queries(BaseModel):\n queries: List[str] = Field(\n description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n )\n\n\ngen_queries_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\ngen_queries_chain = gen_queries_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Queries, include_raw=True)"] }, { "cell_type": "code", @@ -642,128 +324,28 @@ "output_type": "execute_result" } ], - "source": [ - "queries = await gen_queries_chain.ainvoke(\n", - " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", - ")\n", - "queries[\"parsed\"].queries" - ] + "source": ["queries = await gen_queries_chain.ainvoke(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nqueries[\"parsed\"].queries"] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], - "source": [ - "class AnswerWithCitations(BaseModel):\n", - " answer: str = Field(\n", - " description=\"Comprehensive answer to the user's question with citations.\",\n", - " )\n", - " cited_urls: List[str] = Field(\n", - " description=\"List of urls cited in the answer.\",\n", - " )\n", - "\n", - " @property\n", - " def as_str(self) -> str:\n", - " return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n", - " f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n", - " )\n", - "\n", - "\n", - "gen_answer_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n", - " to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n", - "\n", - "Make your response as informative as possible and make sure every sentence is supported by the gathered information.\n", - "Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n", - " ),\n", - " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", - " ]\n", - ")\n", - "\n", - "gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n", - " AnswerWithCitations, include_raw=True\n", - ").with_config(run_name=\"GenerateAnswer\")" - ] + "source": ["class AnswerWithCitations(BaseModel):\n answer: str = Field(\n description=\"Comprehensive answer to the user's question with citations.\",\n )\n cited_urls: List[str] = Field(\n description=\"List of urls cited in the answer.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n )\n\n\ngen_answer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n\nMake your response as informative as possible and make sure every sentence is supported by the gathered information.\nEach response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\ngen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n AnswerWithCitations, include_raw=True\n).with_config(run_name=\"GenerateAnswer\")"] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\n", - "from langchain_core.tools import tool\n", - "\n", - "'''\n", - "# Tavily is typically a better search engine, but your free queries are limited\n", - "search_engine = TavilySearchResults(max_results=4)\n", - "\n", - "@tool\n", - "async def search_engine(query: str):\n", - " \"\"\"Search engine to the internet.\"\"\"\n", - " results = tavily_search.invoke(query)\n", - " return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n", - "'''\n", - "\n", - "# DDG\n", - "search_engine = DuckDuckGoSearchAPIWrapper()\n", - "\n", - "\n", - "@tool\n", - "async def search_engine(query: str):\n", - " \"\"\"Search engine to the internet.\"\"\"\n", - " results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n", - " return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" - ] + "source": ["from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\nfrom langchain_core.tools import tool\n\n'''\n# Tavily is typically a better search engine, but your free queries are limited\nsearch_engine = TavilySearchResults(max_results=4)\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = tavily_search.invoke(query)\n return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n'''\n\n# DDG\nsearch_engine = DuckDuckGoSearchAPIWrapper()\n\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]"] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "\n", - "from langchain_core.runnables import RunnableConfig\n", - "\n", - "\n", - "async def gen_answer(\n", - " state: InterviewState,\n", - " config: Optional[RunnableConfig] = None,\n", - " name: str = \"Subject_Matter_Expert\",\n", - " max_str_len: int = 15000,\n", - "):\n", - " swapped_state = swap_roles(state, name) # Convert all other AI messages\n", - " queries = await gen_queries_chain.ainvoke(swapped_state)\n", - " query_results = await search_engine.abatch(\n", - " queries[\"parsed\"].queries, config, return_exceptions=True\n", - " )\n", - " successful_results = [\n", - " res for res in query_results if not isinstance(res, Exception)\n", - " ]\n", - " all_query_results = {\n", - " res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n", - " }\n", - " # We could be more precise about handling max token length if we wanted to here\n", - " dumped = json.dumps(all_query_results)[:max_str_len]\n", - " ai_message: AIMessage = queries[\"raw\"]\n", - " tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n", - " tool_id = tool_call[\"id\"]\n", - " tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n", - " swapped_state[\"messages\"].extend([ai_message, tool_message])\n", - " # Only update the shared state with the final answer to avoid\n", - " # polluting the dialogue history with intermediate messages\n", - " generated = await gen_answer_chain.ainvoke(swapped_state)\n", - " cited_urls = set(generated[\"parsed\"].cited_urls)\n", - " # Save the retrieved information to a the shared state for future reference\n", - " cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n", - " formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n", - " return {\"messages\": [formatted_message], \"references\": cited_references}" - ] + "source": ["import json\n\nfrom langchain_core.runnables import RunnableConfig\n\n\nasync def gen_answer(\n state: InterviewState,\n config: Optional[RunnableConfig] = None,\n name: str = \"Subject_Matter_Expert\",\n max_str_len: int = 15000,\n):\n swapped_state = swap_roles(state, name) # Convert all other AI messages\n queries = await gen_queries_chain.ainvoke(swapped_state)\n query_results = await search_engine.abatch(\n queries[\"parsed\"].queries, config, return_exceptions=True\n )\n successful_results = [\n res for res in query_results if not isinstance(res, Exception)\n ]\n all_query_results = {\n res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n }\n # We could be more precise about handling max token length if we wanted to here\n dumped = json.dumps(all_query_results)[:max_str_len]\n ai_message: AIMessage = queries[\"raw\"]\n tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n tool_id = tool_call[\"id\"]\n tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n swapped_state[\"messages\"].extend([ai_message, tool_message])\n # Only update the shared state with the final answer to avoid\n # polluting the dialogue history with intermediate messages\n generated = await gen_answer_chain.ainvoke(swapped_state)\n cited_urls = set(generated[\"parsed\"].cited_urls)\n # Save the retrieved information to a the shared state for future reference\n cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n return {\"messages\": [formatted_message], \"references\": cited_references}"] }, { "cell_type": "code", @@ -781,12 +363,7 @@ "output_type": "execute_result" } ], - "source": [ - "example_answer = await gen_answer(\n", - " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", - ")\n", - "example_answer[\"messages\"][-1].content" - ] + "source": ["example_answer = await gen_answer(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nexample_answer[\"messages\"][-1].content"] }, { "cell_type": "markdown", @@ -803,33 +380,7 @@ "execution_count": 45, "metadata": {}, "outputs": [], - "source": [ - "max_num_turns = 5\n", - "\n", - "\n", - "def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n", - " messages = state[\"messages\"]\n", - " num_responses = len(\n", - " [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n", - " )\n", - " if num_responses >= max_num_turns:\n", - " return END\n", - " last_question = messages[-2]\n", - " if last_question.content.endswith(\"Thank you so much for your help!\"):\n", - " return END\n", - " return \"ask_question\"\n", - "\n", - "\n", - "builder = StateGraph(InterviewState)\n", - "\n", - "builder.add_node(\"ask_question\", generate_question)\n", - "builder.add_node(\"answer_question\", gen_answer)\n", - "builder.add_conditional_edges(\"answer_question\", route_messages)\n", - "builder.add_edge(\"ask_question\", \"answer_question\")\n", - "\n", - "builder.set_entry_point(\"ask_question\")\n", - "interview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" - ] + "source": ["max_num_turns = 5\n\n\ndef route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n messages = state[\"messages\"]\n num_responses = len(\n [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n )\n if num_responses >= max_num_turns:\n return END\n last_question = messages[-2]\n if last_question.content.endswith(\"Thank you so much for your help!\"):\n return END\n return \"ask_question\"\n\n\nbuilder = StateGraph(InterviewState)\n\nbuilder.add_node(\"ask_question\", generate_question)\nbuilder.add_node(\"answer_question\", gen_answer)\nbuilder.add_conditional_edges(\"answer_question\", route_messages)\nbuilder.add_edge(\"ask_question\", \"answer_question\")\n\nbuilder.add_edge(START, \"ask_question\")\ninterview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")"] }, { "cell_type": "code", @@ -848,13 +399,7 @@ "output_type": "execute_result" } ], - "source": [ - "from IPython.display import Image\n", - "\n", - "# Feel free to comment out if you have\n", - "# not installed pygraphviz\n", - "Image(interview_graph.get_graph().draw_png())" - ] + "source": ["from IPython.display import Image\n\n# Feel free to comment out if you have\n# not installed pygraphviz\nImage(interview_graph.get_graph().draw_png())"] }, { "cell_type": "code", @@ -882,34 +427,14 @@ ] } ], - "source": [ - "final_step = None\n", - "\n", - "initial_state = {\n", - " \"editor\": perspectives.editors[0],\n", - " \"messages\": [\n", - " AIMessage(\n", - " content=f\"So you said you were writing an article on {example_topic}?\",\n", - " name=\"Subject_Matter_Expert\",\n", - " )\n", - " ],\n", - "}\n", - "async for step in interview_graph.astream(initial_state):\n", - " name = next(iter(step))\n", - " print(name)\n", - " print(\"-- \", str(step[name][\"messages\"])[:300])\n", - " if END in step:\n", - " final_step = step" - ] + "source": ["final_step = None\n\ninitial_state = {\n \"editor\": perspectives.editors[0],\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {example_topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n}\nasync for step in interview_graph.astream(initial_state):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name][\"messages\"])[:300])\n if END in step:\n final_step = step"] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], - "source": [ - "final_state = next(iter(final_step.values()))" - ] + "source": ["final_state = next(iter(final_step.values()))"] }, { "cell_type": "markdown", @@ -925,48 +450,14 @@ "execution_count": 53, "metadata": {}, "outputs": [], - "source": [ - "refine_outline_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\n", - "You need to make sure that the outline is comprehensive and specific. \\\n", - "Topic you are writing about: {topic} \n", - "\n", - "Old outline:\n", - "\n", - "{old_outline}\"\"\",\n", - " ),\n", - " (\n", - " \"user\",\n", - " \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n", - " ),\n", - " ]\n", - ")\n", - "\n", - "# Using turbo preview since the context can get quite long\n", - "refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n", - " Outline\n", - ")" - ] + "source": ["refine_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\nYou need to make sure that the outline is comprehensive and specific. \\\nTopic you are writing about: {topic} \n\nOld outline:\n\n{old_outline}\"\"\",\n ),\n (\n \"user\",\n \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n ),\n ]\n)\n\n# Using turbo preview since the context can get quite long\nrefine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n Outline\n)"] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], - "source": [ - "refined_outline = refine_outline_chain.invoke(\n", - " {\n", - " \"topic\": example_topic,\n", - " \"old_outline\": initial_outline.as_str,\n", - " \"conversations\": \"\\n\\n\".join(\n", - " f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n", - " ),\n", - " }\n", - ")" - ] + "source": ["refined_outline = refine_outline_chain.invoke(\n {\n \"topic\": example_topic,\n \"old_outline\": initial_outline.as_str,\n \"conversations\": \"\\n\\n\".join(\n f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n ),\n }\n)"] }, { "cell_type": "code", @@ -1025,9 +516,7 @@ ] } ], - "source": [ - "print(refined_outline.as_str)" - ] + "source": ["print(refined_outline.as_str)"] }, { "cell_type": "markdown", @@ -1049,25 +538,7 @@ "execution_count": 28, "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.vectorstores import SKLearnVectorStore\n", - "from langchain_core.documents import Document\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n", - "reference_docs = [\n", - " Document(page_content=v, metadata={\"source\": k})\n", - " for k, v in final_state[\"references\"].items()\n", - "]\n", - "# This really doesn't need to be a vectorstore for this size of data.\n", - "# It could just be a numpy matrix. Or you could store documents\n", - "# across requests if you want.\n", - "vectorstore = SKLearnVectorStore.from_documents(\n", - " reference_docs,\n", - " embedding=embeddings,\n", - ")\n", - "retriever = vectorstore.as_retriever(k=10)" - ] + "source": ["from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_core.documents import Document\nfrom langchain_openai import OpenAIEmbeddings\n\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nreference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in final_state[\"references\"].items()\n]\n# This really doesn't need to be a vectorstore for this size of data.\n# It could just be a numpy matrix. Or you could store documents\n# across requests if you want.\nvectorstore = SKLearnVectorStore.from_documents(\n reference_docs,\n embedding=embeddings,\n)\nretriever = vectorstore.as_retriever(k=10)"] }, { "cell_type": "code", @@ -1088,9 +559,7 @@ "output_type": "execute_result" } ], - "source": [ - "retriever.invoke(\"What's a long context LLM anyway?\")" - ] + "source": ["retriever.invoke(\"What's a long context LLM anyway?\")"] }, { "cell_type": "markdown", @@ -1106,69 +575,7 @@ "execution_count": 30, "metadata": {}, "outputs": [], - "source": [ - "class SubSection(BaseModel):\n", - " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", - " content: str = Field(\n", - " ...,\n", - " title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n", - " )\n", - "\n", - " @property\n", - " def as_str(self) -> str:\n", - " return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n", - "\n", - "\n", - "class WikiSection(BaseModel):\n", - " section_title: str = Field(..., title=\"Title of the section\")\n", - " content: str = Field(..., title=\"Full content of the section\")\n", - " subsections: Optional[List[Subsection]] = Field(\n", - " default=None,\n", - " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", - " )\n", - " citations: List[str] = Field(default_factory=list)\n", - "\n", - " @property\n", - " def as_str(self) -> str:\n", - " subsections = \"\\n\\n\".join(\n", - " subsection.as_str for subsection in self.subsections or []\n", - " )\n", - " citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n", - " return (\n", - " f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n", - " + f\"\\n\\n{citations}\".strip()\n", - " )\n", - "\n", - "\n", - "section_writer_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n", - " \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n", - " ),\n", - " (\"user\", \"Write the full WikiSection for the {section} section.\"),\n", - " ]\n", - ")\n", - "\n", - "\n", - "async def retrieve(inputs: dict):\n", - " docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n", - " formatted = \"\\n\".join(\n", - " [\n", - " f'\\n{doc.page_content}\\n'\n", - " for doc in docs\n", - " ]\n", - " )\n", - " return {\"docs\": formatted, **inputs}\n", - "\n", - "\n", - "section_writer = (\n", - " retrieve\n", - " | section_writer_prompt\n", - " | long_context_llm.with_structured_output(WikiSection)\n", - ")" - ] + "source": ["class SubSection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n content: str = Field(\n ...,\n title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n\n\nclass WikiSection(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n content: str = Field(..., title=\"Full content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n citations: List[str] = Field(default_factory=list)\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n subsection.as_str for subsection in self.subsections or []\n )\n citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n return (\n f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n + f\"\\n\\n{citations}\".strip()\n )\n\n\nsection_writer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n ),\n (\"user\", \"Write the full WikiSection for the {section} section.\"),\n ]\n)\n\n\nasync def retrieve(inputs: dict):\n docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n formatted = \"\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )\n return {\"docs\": formatted, **inputs}\n\n\nsection_writer = (\n retrieve\n | section_writer_prompt\n | long_context_llm.with_structured_output(WikiSection)\n)"] }, { "cell_type": "code", @@ -1193,16 +600,7 @@ ] } ], - "source": [ - "section = await section_writer.ainvoke(\n", - " {\n", - " \"outline\": refined_outline.as_str,\n", - " \"section\": refined_outline.sections[1].section_title,\n", - " \"topic\": example_topic,\n", - " }\n", - ")\n", - "print(section.as_str)" - ] + "source": ["section = await section_writer.ainvoke(\n {\n \"outline\": refined_outline.as_str,\n \"section\": refined_outline.sections[1].section_title,\n \"topic\": example_topic,\n }\n)\nprint(section.as_str)"] }, { "cell_type": "markdown", @@ -1218,26 +616,7 @@ "execution_count": 32, "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.output_parsers import StrOutputParser\n", - "\n", - "writer_prompt = ChatPromptTemplate.from_messages(\n", - " [\n", - " (\n", - " \"system\",\n", - " \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n", - " \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n", - " ),\n", - " (\n", - " \"user\",\n", - " 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n", - " \" avoiding duplicates in the footer. Include URLs in the footer.\",\n", - " ),\n", - " ]\n", - ")\n", - "\n", - "writer = writer_prompt | long_context_llm | StrOutputParser()" - ] + "source": ["from langchain_core.output_parsers import StrOutputParser\n\nwriter_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n ),\n (\n \"user\",\n 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n \" avoiding duplicates in the footer. Include URLs in the footer.\",\n ),\n ]\n)\n\nwriter = writer_prompt | long_context_llm | StrOutputParser()"] }, { "cell_type": "code", @@ -1328,10 +707,7 @@ ] } ], - "source": [ - "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n", - " print(tok, end=\"\")" - ] + "source": ["for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n print(tok, end=\"\")"] }, { "cell_type": "markdown", @@ -1356,127 +732,14 @@ "execution_count": 55, "metadata": {}, "outputs": [], - "source": [ - "class ResearchState(TypedDict):\n", - " topic: str\n", - " outline: Outline\n", - " editors: List[Editor]\n", - " interview_results: List[InterviewState]\n", - " # The final sections output\n", - " sections: List[WikiSection]\n", - " article: str" - ] + "source": ["class ResearchState(TypedDict):\n topic: str\n outline: Outline\n editors: List[Editor]\n interview_results: List[InterviewState]\n # The final sections output\n sections: List[WikiSection]\n article: str"] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [], - "source": [ - "import asyncio\n", - "\n", - "\n", - "async def initialize_research(state: ResearchState):\n", - " topic = state[\"topic\"]\n", - " coros = (\n", - " generate_outline_direct.ainvoke({\"topic\": topic}),\n", - " survey_subjects.ainvoke(topic),\n", - " )\n", - " results = await asyncio.gather(*coros)\n", - " return {\n", - " **state,\n", - " \"outline\": results[0],\n", - " \"editors\": results[1].editors,\n", - " }\n", - "\n", - "\n", - "async def conduct_interviews(state: ResearchState):\n", - " topic = state[\"topic\"]\n", - " initial_states = [\n", - " {\n", - " \"editor\": editor,\n", - " \"messages\": [\n", - " AIMessage(\n", - " content=f\"So you said you were writing an article on {topic}?\",\n", - " name=\"Subject_Matter_Expert\",\n", - " )\n", - " ],\n", - " }\n", - " for editor in state[\"editors\"]\n", - " ]\n", - " # We call in to the sub-graph here to parallelize the interviews\n", - " interview_results = await interview_graph.abatch(initial_states)\n", - "\n", - " return {\n", - " **state,\n", - " \"interview_results\": interview_results,\n", - " }\n", - "\n", - "\n", - "def format_conversation(interview_state):\n", - " messages = interview_state[\"messages\"]\n", - " convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n", - " return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n", - "\n", - "\n", - "async def refine_outline(state: ResearchState):\n", - " convos = \"\\n\\n\".join(\n", - " [\n", - " format_conversation(interview_state)\n", - " for interview_state in state[\"interview_results\"]\n", - " ]\n", - " )\n", - "\n", - " updated_outline = await refine_outline_chain.ainvoke(\n", - " {\n", - " \"topic\": state[\"topic\"],\n", - " \"old_outline\": state[\"outline\"].as_str,\n", - " \"conversations\": convos,\n", - " }\n", - " )\n", - " return {**state, \"outline\": updated_outline}\n", - "\n", - "\n", - "async def index_references(state: ResearchState):\n", - " all_docs = []\n", - " for interview_state in state[\"interview_results\"]:\n", - " reference_docs = [\n", - " Document(page_content=v, metadata={\"source\": k})\n", - " for k, v in interview_state[\"references\"].items()\n", - " ]\n", - " all_docs.extend(reference_docs)\n", - " await vectorstore.aadd_documents(all_docs)\n", - " return state\n", - "\n", - "\n", - "async def write_sections(state: ResearchState):\n", - " outline = state[\"outline\"]\n", - " sections = await section_writer.abatch(\n", - " [\n", - " {\n", - " \"outline\": refined_outline.as_str,\n", - " \"section\": section.section_title,\n", - " \"topic\": state[\"topic\"],\n", - " }\n", - " for section in outline.sections\n", - " ]\n", - " )\n", - " return {\n", - " **state,\n", - " \"sections\": sections,\n", - " }\n", - "\n", - "\n", - "async def write_article(state: ResearchState):\n", - " topic = state[\"topic\"]\n", - " sections = state[\"sections\"]\n", - " draft = \"\\n\\n\".join([section.as_str for section in sections])\n", - " article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n", - " return {\n", - " **state,\n", - " \"article\": article,\n", - " }" - ] + "source": ["import asyncio\n\n\nasync def initialize_research(state: ResearchState):\n topic = state[\"topic\"]\n coros = (\n generate_outline_direct.ainvoke({\"topic\": topic}),\n survey_subjects.ainvoke(topic),\n )\n results = await asyncio.gather(*coros)\n return {\n **state,\n \"outline\": results[0],\n \"editors\": results[1].editors,\n }\n\n\nasync def conduct_interviews(state: ResearchState):\n topic = state[\"topic\"]\n initial_states = [\n {\n \"editor\": editor,\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n }\n for editor in state[\"editors\"]\n ]\n # We call in to the sub-graph here to parallelize the interviews\n interview_results = await interview_graph.abatch(initial_states)\n\n return {\n **state,\n \"interview_results\": interview_results,\n }\n\n\ndef format_conversation(interview_state):\n messages = interview_state[\"messages\"]\n convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n\n\nasync def refine_outline(state: ResearchState):\n convos = \"\\n\\n\".join(\n [\n format_conversation(interview_state)\n for interview_state in state[\"interview_results\"]\n ]\n )\n\n updated_outline = await refine_outline_chain.ainvoke(\n {\n \"topic\": state[\"topic\"],\n \"old_outline\": state[\"outline\"].as_str,\n \"conversations\": convos,\n }\n )\n return {**state, \"outline\": updated_outline}\n\n\nasync def index_references(state: ResearchState):\n all_docs = []\n for interview_state in state[\"interview_results\"]:\n reference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in interview_state[\"references\"].items()\n ]\n all_docs.extend(reference_docs)\n await vectorstore.aadd_documents(all_docs)\n return state\n\n\nasync def write_sections(state: ResearchState):\n outline = state[\"outline\"]\n sections = await section_writer.abatch(\n [\n {\n \"outline\": refined_outline.as_str,\n \"section\": section.section_title,\n \"topic\": state[\"topic\"],\n }\n for section in outline.sections\n ]\n )\n return {\n **state,\n \"sections\": sections,\n }\n\n\nasync def write_article(state: ResearchState):\n topic = state[\"topic\"]\n sections = state[\"sections\"]\n draft = \"\\n\\n\".join([section.as_str for section in sections])\n article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n return {\n **state,\n \"article\": article,\n }"] }, { "cell_type": "markdown", @@ -1490,29 +753,7 @@ "execution_count": 73, "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "builder_of_storm = StateGraph(ResearchState)\n", - "\n", - "nodes = [\n", - " (\"init_research\", initialize_research),\n", - " (\"conduct_interviews\", conduct_interviews),\n", - " (\"refine_outline\", refine_outline),\n", - " (\"index_references\", index_references),\n", - " (\"write_sections\", write_sections),\n", - " (\"write_article\", write_article),\n", - "]\n", - "for i in range(len(nodes)):\n", - " name, node = nodes[i]\n", - " builder_of_storm.add_node(name, node)\n", - " if i > 0:\n", - " builder_of_storm.add_edge(nodes[i - 1][0], name)\n", - "\n", - "builder_of_storm.set_entry_point(nodes[0][0])\n", - "builder_of_storm.set_finish_point(nodes[-1][0])\n", - "storm = builder_of_storm.compile(checkpointer=MemorySaver())" - ] + "source": ["from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.set_finish_point(nodes[-1][0])\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())"] }, { "cell_type": "code", @@ -1531,9 +772,7 @@ "output_type": "execute_result" } ], - "source": [ - "Image(storm.get_graph().draw_png())" - ] + "source": ["Image(storm.get_graph().draw_png())"] }, { "cell_type": "code", @@ -1561,28 +800,14 @@ ] } ], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\n", - "async for step in storm.astream(\n", - " {\n", - " \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n", - " },\n", - " config,\n", - "):\n", - " name = next(iter(step))\n", - " print(name)\n", - " print(\"-- \", str(step[name])[:300])" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\nasync for step in storm.astream(\n {\n \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n },\n config,\n):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name])[:300])"] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [], - "source": [ - "checkpoint = storm.get_state(config)\n", - "article = checkpoint.values[\"article\"]" - ] + "source": ["checkpoint = storm.get_state(config)\narticle = checkpoint.values[\"article\"]"] }, { "cell_type": "markdown", @@ -1661,19 +886,14 @@ "output_type": "execute_result" } ], - "source": [ - "from IPython.display import Markdown\n", - "\n", - "# We will down-header the sections to create less confusion in this notebook\n", - "Markdown(article.replace(\"\\n#\", \"\\n##\"))" - ] + "source": ["from IPython.display import Markdown\n\n# We will down-header the sections to create less confusion in this notebook\nMarkdown(article.replace(\"\\n#\", \"\\n##\"))"] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/streaming-from-final-node.ipynb b/examples/streaming-from-final-node.ipynb index d05fce0b0..bef24600a 100644 --- a/examples/streaming-from-final-node.ipynb +++ b/examples/streaming-from-final-node.ipynb @@ -22,10 +22,7 @@ "id": "c04a3f8e-0bc9-430b-85db-3edfa026d2cd", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langchain-openai" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-openai"] }, { "cell_type": "code", @@ -41,18 +38,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -68,35 +54,7 @@ "id": "1d51c35c-dbf2-4c01-932d-c5d308ea37d2", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.runnables import ConfigurableField\n", - "from langchain_core.tools import tool\n", - "from langchain_openai import ChatOpenAI\n", - "from langgraph.prebuilt import create_react_agent\n", - "from langgraph.prebuilt import ToolNode\n", - "\n", - "\n", - "@tool\n", - "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", - " \"\"\"Use this to get weather information.\"\"\"\n", - " if city == \"nyc\":\n", - " return \"It might be cloudy in nyc\"\n", - " elif city == \"sf\":\n", - " return \"It's always sunny in sf\"\n", - " else:\n", - " raise AssertionError(\"Unknown city\")\n", - "\n", - "\n", - "tools = [get_weather]\n", - "model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", - "final_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", - "\n", - "model = model.bind_tools(tools)\n", - "# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\n", - "final_model = final_model.with_config(tags=[\"final_node\"])" - ] + "source": ["from typing import Literal\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.runnables import ConfigurableField\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef get_weather(city: Literal[\"nyc\", \"sf\"]):\n \"\"\"Use this to get weather information.\"\"\"\n if city == \"nyc\":\n return \"It might be cloudy in nyc\"\n elif city == \"sf\":\n return \"It's always sunny in sf\"\n else:\n raise AssertionError(\"Unknown city\")\n\n\ntools = [get_weather]\nmodel = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\nfinal_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\nmodel = model.bind_tools(tools)\n# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\nfinal_model = final_model.with_config(tags=[\"final_node\"])"] }, { "cell_type": "code", @@ -104,9 +62,7 @@ "id": "0af37212-e592-484d-9194-35d53fa79678", "metadata": {}, "outputs": [], - "source": [ - "tool_node = ToolNode(tools=tools)" - ] + "source": ["tool_node = ToolNode(tools=tools)"] }, { "cell_type": "code", @@ -114,13 +70,7 @@ "id": "ac9d4f5b-655a-48f3-b514-a4a0815714a6", "metadata": {}, "outputs": [], - "source": [ - "from typing import TypedDict, Annotated\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.graph.message import MessagesState\n", - "from langchain_core.messages import BaseMessage" - ] + "source": ["from typing import TypedDict, Annotated\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import MessagesState\nfrom langchain_core.messages import BaseMessage"] }, { "cell_type": "markdown", @@ -136,9 +86,7 @@ "id": "3948c6b8-0317-4001-b699-32b25306a023", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import SystemMessage, HumanMessage" - ] + "source": ["from langchain_core.messages import SystemMessage, HumanMessage"] }, { "cell_type": "code", @@ -146,35 +94,7 @@ "id": "2efe9fb4-c6c2-4171-becd-d45bbf899209", "metadata": {}, "outputs": [], - "source": [ - "def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n", - " messages = state['messages']\n", - " last_message = messages[-1]\n", - " # If the LLM makes a tool call, then we route to the \"tools\" node\n", - " if last_message.tool_calls:\n", - " return \"tools\"\n", - " # Otherwise, we stop (reply to the user)\n", - " return \"final\"\n", - "\n", - "\n", - "def call_model(state: MessagesState):\n", - " messages = state['messages']\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "\n", - "\n", - "def call_final_model(state: MessagesState):\n", - " messages = state['messages']\n", - " last_ai_message = messages[-1]\n", - " response = final_model.invoke([\n", - " SystemMessage(\"Rewrite this in the voice of Al Roker\"),\n", - " HumanMessage(last_ai_message.content)\n", - " ])\n", - " # overwrite the last AI message from the agent\n", - " response.id = last_ai_message.id\n", - " return {\"messages\": [response]}" - ] + "source": ["def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n messages = state['messages']\n last_message = messages[-1]\n # If the LLM makes a tool call, then we route to the \"tools\" node\n if last_message.tool_calls:\n return \"tools\"\n # Otherwise, we stop (reply to the user)\n return \"final\"\n\n\ndef call_model(state: MessagesState):\n messages = state['messages']\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef call_final_model(state: MessagesState):\n messages = state['messages']\n last_ai_message = messages[-1]\n response = final_model.invoke([\n SystemMessage(\"Rewrite this in the voice of Al Roker\"),\n HumanMessage(last_ai_message.content)\n ])\n # overwrite the last AI message from the agent\n response.id = last_ai_message.id\n return {\"messages\": [response]}"] }, { "cell_type": "code", @@ -182,23 +102,7 @@ "id": "b1a9a981-8629-4d25-a0e1-d666c3968b30", "metadata": {}, "outputs": [], - "source": [ - "workflow = StateGraph(MessagesState)\n", - "\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"tools\", tool_node)\n", - "# add a separate final node\n", - "workflow.add_node(\"final\", call_final_model)\n", - "\n", - "workflow.set_entry_point(\"agent\")\n", - "workflow.add_conditional_edges(\n", - " \"agent\",\n", - " should_continue,\n", - ")\n", - "\n", - "workflow.add_edge(\"tools\", 'agent')\n", - "workflow.add_edge(\"final\", END)" - ] + "source": ["workflow = StateGraph(MessagesState)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"tools\", tool_node)\n# add a separate final node\nworkflow.add_node(\"final\", call_final_model)\n\nworkflow.add_edge(START, \"agent\")\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n)\n\nworkflow.add_edge(\"tools\", 'agent')\nworkflow.add_edge(\"final\", END)"] }, { "cell_type": "code", @@ -206,9 +110,7 @@ "id": "a7b0251f-dcee-49d6-8133-af50d4a55e22", "metadata": {}, "outputs": [], - "source": [ - "app = workflow.compile()" - ] + "source": ["app = workflow.compile()"] }, { "cell_type": "code", @@ -216,9 +118,7 @@ "id": "f8b77e74-17e9-4fee-a164-4637013b55ff", "metadata": {}, "outputs": [], - "source": [ - "from IPython.display import display, Image" - ] + "source": ["from IPython.display import display, Image"] }, { "cell_type": "code", @@ -237,9 +137,7 @@ "output_type": "display_data" } ], - "source": [ - "display(Image(app.get_graph().draw_mermaid_png()))" - ] + "source": ["display(Image(app.get_graph().draw_mermaid_png()))"] }, { "cell_type": "markdown", @@ -271,19 +169,7 @@ ] } ], - "source": [ - "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\n", - "async for event in app.astream_events(inputs, version=\"v2\"):\n", - " kind = event[\"event\"]\n", - " tags = event.get(\"tags\", [])\n", - " if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n", - " data = event[\"data\"]\n", - " if data[\"chunk\"].content:\n", - " # Empty content in the context of OpenAI or Anthropic usually means\n", - " # that the model is asking for a tool to be invoked.\n", - " # So we only print non-empty content\n", - " print(data[\"chunk\"].content, end=\"|\")" - ] + "source": ["inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\nasync for event in app.astream_events(inputs, version=\"v2\"):\n kind = event[\"event\"]\n tags = event.get(\"tags\", [])\n if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n data = event[\"data\"]\n if data[\"chunk\"].content:\n # Empty content in the context of OpenAI or Anthropic usually means\n # that the model is asking for a tool to be invoked.\n # So we only print non-empty content\n print(data[\"chunk\"].content, end=\"|\")"] } ], "metadata": { diff --git a/examples/subgraph.ipynb b/examples/subgraph.ipynb index f92d59733..cbdd2cbf9 100644 --- a/examples/subgraph.ipynb +++ b/examples/subgraph.ipynb @@ -22,10 +22,7 @@ "execution_count": 2, "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph"] }, { "cell_type": "markdown", @@ -39,19 +36,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -69,60 +54,7 @@ "execution_count": 1, "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " return left + right\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.set_entry_point(\"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.set_finish_point(\"child_end\")\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.set_entry_point(\"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.set_finish_point(\"fin\")\n", - "graph = builder.compile()" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n if not left:\n left = []\n if not right:\n right = []\n return left + right\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nchild_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.set_finish_point(\"child_end\")\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.set_finish_point(\"fin\")\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -140,12 +72,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))"] }, { "cell_type": "code", @@ -235,9 +162,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"name\": \"test\"}, debug=True)" - ] + "source": ["graph.invoke({\"name\": \"test\"}, debug=True)"] }, { "cell_type": "markdown", @@ -257,79 +182,14 @@ "execution_count": 23, "metadata": {}, "outputs": [], - "source": [ - "import uuid\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " left_, right_ = [], []\n", - " for orig, new in [(left, left_), (right, right_)]:\n", - " for val in orig:\n", - " if not isinstance(val, dict):\n", - " val = {\"val\": val}\n", - " if \"id\" not in val:\n", - " val[\"id\"] = str(uuid.uuid4())\n", - " new.append(val)\n", - " # Merge the two lists\n", - " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n", - " merged = left_.copy()\n", - " for val in right_:\n", - " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n", - " merged[existing_idx] = val\n", - " else:\n", - " merged.append(val)\n", - " return merged\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]" - ] + "source": ["import uuid\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n if not left:\n left = []\n if not right:\n right = []\n left_, right_ = [], []\n for orig, new in [(left, left_), (right, right_)]:\n for val in orig:\n if not isinstance(val, dict):\n val = {\"val\": val}\n if \"id\" not in val:\n val[\"id\"] = str(uuid.uuid4())\n new.append(val)\n # Merge the two lists\n left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n merged = left_.copy()\n for val in right_:\n if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n merged[existing_idx] = val\n else:\n merged.append(val)\n return merged\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]"] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], - "source": [ - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.set_entry_point(\"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.set_finish_point(\"child_end\")\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.set_entry_point(\"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.set_finish_point(\"fin\")\n", - "graph = builder.compile()" - ] + "source": ["child_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.set_finish_point(\"child_end\")\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.set_finish_point(\"fin\")\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -347,12 +207,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] + "source": ["from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))"] }, { "cell_type": "code", @@ -446,9 +301,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.invoke({\"name\": \"test\"}, debug=True)" - ] + "source": ["graph.invoke({\"name\": \"test\"}, debug=True)"] } ], "metadata": { diff --git a/examples/time-travel.ipynb b/examples/time-travel.ipynb index b782cec30..4eccaf4e5 100644 --- a/examples/time-travel.ipynb +++ b/examples/time-travel.ipynb @@ -46,10 +46,7 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"] }, { "cell_type": "markdown", @@ -65,18 +62,7 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] }, { "cell_type": "markdown", @@ -92,10 +78,7 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "_set_env(\"LANGCHAIN_API_KEY\")" - ] + "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] }, { "cell_type": "markdown", @@ -113,22 +96,7 @@ "id": "f5319e01", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "# `add_messages`` essentially does this\n", - "# (with more robust handling)\n", - "# def add_messages(left: list, right: list):\n", - "# return left + right\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] }, { "cell_type": "markdown", @@ -148,19 +116,7 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def search(query: str):\n", - " \"\"\"Call to surf the web.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " return [\"The weather is cloudy with a chance of meatballs.\"]\n", - "\n", - "\n", - "tools = [search]" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The weather is cloudy with a chance of meatballs.\"]\n\n\ntools = [search]"] }, { "cell_type": "markdown", @@ -177,11 +133,7 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] }, { "cell_type": "markdown", @@ -205,11 +157,7 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0)" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] }, { "cell_type": "markdown", @@ -227,9 +175,7 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": [ - "model = model.bind_tools(tools)" - ] + "source": ["model = model.bind_tools(tools)"] }, { "cell_type": "markdown", @@ -264,20 +210,7 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": [ - "from typing import Literal\n", - "\n", - "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n", - " last_message = state[\"messages\"][-1]\n", - " # If there is no function call, then we finish\n", - " if not last_message.tool_calls:\n", - " return \"end\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"" - ] + "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""] }, { "cell_type": "markdown", @@ -295,50 +228,7 @@ "id": "812b4e70-4956-4415-8880-db48b3dcbad2", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "\n", - "# Define the two nodes we will cycle between\n", - "def call_model(state: State) -> State:\n", - " return {\"messages\": model.invoke(state[\"messages\"])}\n", - "\n", - "\n", - "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", tool_node)\n", - "\n", - "# Set the entrypoint as `agent`\n", - "# This means that this node is the first one called\n", - "workflow.set_entry_point(\"agent\")\n", - "\n", - "# We now add a conditional edge\n", - "workflow.add_conditional_edges(\n", - " # First, we define the start node. We use `agent`.\n", - " # This means these are the edges taken after the `agent` node is called.\n", - " \"agent\",\n", - " # Next, we pass in the function that will determine which node is called next.\n", - " should_continue,\n", - " # Finally we pass in a mapping.\n", - " # The keys are strings, and the values are other nodes.\n", - " # END is a special node marking that the graph should finish.\n", - " # What will happen is we will call `should_continue`, and then the output of that\n", - " # will be matched against the keys in this mapping.\n", - " # Based on which one it matches, that node will then be called.\n", - " {\n", - " # If `tools`, then we call the tool node.\n", - " \"continue\": \"action\",\n", - " # Otherwise we finish.\n", - " \"end\": END,\n", - " },\n", - ")\n", - "\n", - "# We now add a normal edge from `tools` to `agent`.\n", - "# This means that after `tools` is called, `agent` node is called next.\n", - "workflow.add_edge(\"action\", \"agent\")" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Define the two nodes we will cycle between\ndef call_model(state: State) -> State:\n return {\"messages\": model.invoke(state[\"messages\"])}\n\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"] }, { "cell_type": "markdown", @@ -356,11 +246,7 @@ "id": "6845ed6a-d155-4105-9160-28849877248b", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "code", @@ -368,12 +254,7 @@ "id": "79d29875-8aa8-434c-9f20-1c58346a6249", "metadata": {}, "outputs": [], - "source": [ - "# Finally, we compile it!\n", - "# This compiles it into a LangChain Runnable,\n", - "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile(checkpointer=memory)" - ] + "source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"] }, { "cell_type": "markdown", @@ -400,15 +281,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(app.get_graph().draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" - ] + "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", @@ -439,14 +312,7 @@ ] } ], - "source": [ - "from langchain_core.messages import HumanMessage\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "input_message = HumanMessage(content=\"hi! I'm bob\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -484,9 +350,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.get_state(config).values" - ] + "source": ["app.get_state(config).values"] }, { "cell_type": "markdown", @@ -515,9 +379,7 @@ "output_type": "execute_result" } ], - "source": [ - "app.get_state(config).next" - ] + "source": ["app.get_state(config).next"] }, { "cell_type": "markdown", @@ -564,12 +426,7 @@ ] } ], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "input_message = HumanMessage(content=\"what is the weather in sf currently\")\n", - "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -609,9 +466,7 @@ "id": "5a68afc0-606f-4294-a872-b2b563be0d69", "metadata": {}, "outputs": [], - "source": [ - "app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" - ] + "source": ["app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] }, { "cell_type": "code", @@ -635,14 +490,7 @@ ] } ], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"4\"}}\n", - "input_message = HumanMessage(content=\"what is the weather in sf currently\")\n", - "for event in app_w_interrupt.stream(\n", - " {\"messages\": [input_message]}, config, stream_mode=\"values\"\n", - "):\n", - " event[\"messages\"][-1].pretty_print()" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"4\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app_w_interrupt.stream(\n {\"messages\": [input_message]}, config, stream_mode=\"values\"\n):\n event[\"messages\"][-1].pretty_print()"] }, { "cell_type": "markdown", @@ -678,10 +526,7 @@ "output_type": "execute_result" } ], - "source": [ - "current_values = app_w_interrupt.get_state(config)\n", - "current_values.next" - ] + "source": ["current_values = app_w_interrupt.get_state(config)\ncurrent_values.next"] }, { "cell_type": "markdown", @@ -710,9 +555,7 @@ "output_type": "execute_result" } ], - "source": [ - "current_values.values[\"messages\"][-1].tool_calls" - ] + "source": ["current_values.values[\"messages\"][-1].tool_calls"] }, { "cell_type": "markdown", @@ -728,11 +571,7 @@ "id": "060e2e33-1f6a-40ef-850e-161b308986fb", "metadata": {}, "outputs": [], - "source": [ - "current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n", - " \"query\"\n", - "] = \"weather in San Francisco today\"" - ] + "source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco today\""] }, { "cell_type": "code", @@ -752,9 +591,7 @@ "output_type": "execute_result" } ], - "source": [ - "app_w_interrupt.update_state(config, current_values.values)" - ] + "source": ["app_w_interrupt.update_state(config, current_values.values)"] }, { "cell_type": "markdown", @@ -792,9 +629,7 @@ "output_type": "execute_result" } ], - "source": [ - "app_w_interrupt.get_state(config).values" - ] + "source": ["app_w_interrupt.get_state(config).values"] }, { "cell_type": "code", @@ -813,9 +648,7 @@ "output_type": "execute_result" } ], - "source": [ - "app_w_interrupt.get_state(config).next" - ] + "source": ["app_w_interrupt.get_state(config).next"] }, { "cell_type": "markdown", @@ -846,11 +679,7 @@ ] } ], - "source": [ - "for event in app_w_interrupt.stream(None, config):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["for event in app_w_interrupt.stream(None, config):\n for v in event.values():\n print(v)"] }, { "cell_type": "markdown", @@ -897,13 +726,7 @@ ] } ], - "source": [ - "for state in app_w_interrupt.get_state_history(config):\n", - " print(state)\n", - " print(\"--\")\n", - " if len(state.values[\"messages\"]) == 2:\n", - " to_replay = state" - ] + "source": ["for state in app_w_interrupt.get_state_history(config):\n print(state)\n print(\"--\")\n if len(state.values[\"messages\"]) == 2:\n to_replay = state"] }, { "cell_type": "markdown", @@ -931,9 +754,7 @@ "output_type": "execute_result" } ], - "source": [ - "to_replay.values" - ] + "source": ["to_replay.values"] }, { "cell_type": "code", @@ -952,9 +773,7 @@ "output_type": "execute_result" } ], - "source": [ - "to_replay.next" - ] + "source": ["to_replay.next"] }, { "cell_type": "markdown", @@ -987,11 +806,7 @@ ] } ], - "source": [ - "for event in app_w_interrupt.stream(None, to_replay.config):\n", - " for v in event.values():\n", - " print(v)" - ] + "source": ["for event in app_w_interrupt.stream(None, to_replay.config):\n for v in event.values():\n print(v)"] }, { "cell_type": "markdown", @@ -1019,18 +834,7 @@ "id": "b084f141-5800-487b-b115-d2e58421b963", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "branch_config = app_w_interrupt.update_state(\n", - " to_replay.config,\n", - " {\n", - " \"messages\": [\n", - " AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n", - " ]\n", - " },\n", - ")" - ] + "source": ["from langchain_core.messages import AIMessage\n\nbranch_config = app_w_interrupt.update_state(\n to_replay.config,\n {\n \"messages\": [\n AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n ]\n },\n)"] }, { "cell_type": "code", @@ -1038,9 +842,7 @@ "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", "metadata": {}, "outputs": [], - "source": [ - "branch_state = app_w_interrupt.get_state(branch_config)" - ] + "source": ["branch_state = app_w_interrupt.get_state(branch_config)"] }, { "cell_type": "code", @@ -1060,9 +862,7 @@ "output_type": "execute_result" } ], - "source": [ - "branch_state.values" - ] + "source": ["branch_state.values"] }, { "cell_type": "code", @@ -1081,9 +881,7 @@ "output_type": "execute_result" } ], - "source": [ - "branch_state.next" - ] + "source": ["branch_state.next"] }, { "cell_type": "markdown", diff --git a/examples/tutorials/sql-agent.ipynb b/examples/tutorials/sql-agent.ipynb index 8d33c5009..4d07901b4 100644 --- a/examples/tutorials/sql-agent.ipynb +++ b/examples/tutorials/sql-agent.ipynb @@ -59,13 +59,7 @@ } }, "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = \"sk-...\"\n", - "os.environ[\"LANGSMITH_API_KEY\"] = \"lsv2_pt_...\"\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" - ] + "source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"sk-...\"\nos.environ[\"LANGSMITH_API_KEY\"] = \"lsv2_pt_...\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""] }, { "cell_type": "code", @@ -73,9 +67,7 @@ "id": "04d73c39-1cc9-4b94-a454-b0a4f604713c", "metadata": {}, "outputs": [], - "source": [ - "os.environ[\"LANGCHAIN_PROJECT\"] = \"sql-agent\"" - ] + "source": ["os.environ[\"LANGCHAIN_PROJECT\"] = \"sql-agent\""] }, { "cell_type": "markdown", @@ -118,22 +110,7 @@ ] } ], - "source": [ - "import requests\n", - "\n", - "url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n", - "\n", - "response = requests.get(url)\n", - "\n", - "if response.status_code == 200:\n", - " # Open a local file in binary write mode\n", - " with open(\"Chinook.db\", \"wb\") as file:\n", - " # Write the content of the response (the file) to the local file\n", - " file.write(response.content)\n", - " print(\"File downloaded and saved as Chinook.db\")\n", - "else:\n", - " print(f\"Failed to download the file. Status code: {response.status_code}\")" - ] + "source": ["import requests\n\nurl = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n\nresponse = requests.get(url)\n\nif response.status_code == 200:\n # Open a local file in binary write mode\n with open(\"Chinook.db\", \"wb\") as file:\n # Write the content of the response (the file) to the local file\n file.write(response.content)\n print(\"File downloaded and saved as Chinook.db\")\nelse:\n print(f\"Failed to download the file. Status code: {response.status_code}\")"] }, { "cell_type": "markdown", @@ -163,10 +140,7 @@ } }, "outputs": [], - "source": [ - "%%capture --no-stderr --no-display\n", - "!pip install langgraph langchain_community langchain_openai" - ] + "source": ["%%capture --no-stderr --no-display\n!pip install langgraph langchain_community langchain_openai"] }, { "cell_type": "code", @@ -202,14 +176,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_community.utilities import SQLDatabase\n", - "\n", - "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", - "print(db.dialect)\n", - "print(db.get_usable_table_names())\n", - "db.run(\"SELECT * FROM Artist LIMIT 10;\")" - ] + "source": ["from langchain_community.utilities import SQLDatabase\n\ndb = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\nprint(db.dialect)\nprint(db.get_usable_table_names())\ndb.run(\"SELECT * FROM Artist LIMIT 10;\")"] }, { "cell_type": "markdown", @@ -241,36 +208,7 @@ } }, "outputs": [], - "source": [ - "from typing import Any\n", - "\n", - "from langchain_core.messages import ToolMessage\n", - "from langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\n", - "from langgraph.prebuilt import ToolNode\n", - "\n", - "\n", - "def create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n", - " \"\"\"\n", - " Create a ToolNode with a fallback to handle errors and surface them to the agent.\n", - " \"\"\"\n", - " return ToolNode(tools).with_fallbacks(\n", - " [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n", - " )\n", - "\n", - "\n", - "def handle_tool_error(state) -> dict:\n", - " error = state.get(\"error\")\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " return {\n", - " \"messages\": [\n", - " ToolMessage(\n", - " content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " for tc in tool_calls\n", - " ]\n", - " }" - ] + "source": ["from typing import Any\n\nfrom langchain_core.messages import ToolMessage\nfrom langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\nfrom langgraph.prebuilt import ToolNode\n\n\ndef create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n \"\"\"\n Create a ToolNode with a fallback to handle errors and surface them to the agent.\n \"\"\"\n return ToolNode(tools).with_fallbacks(\n [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n )\n\n\ndef handle_tool_error(state) -> dict:\n error = state.get(\"error\")\n tool_calls = state[\"messages\"][-1].tool_calls\n return {\n \"messages\": [\n ToolMessage(\n content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n tool_call_id=tc[\"id\"],\n )\n for tc in tool_calls\n ]\n }"] }, { "cell_type": "markdown", @@ -330,20 +268,7 @@ ] } ], - "source": [ - "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "toolkit = SQLDatabaseToolkit(db=db, llm=ChatOpenAI(model=\"gpt-4o\"))\n", - "tools = toolkit.get_tools()\n", - "\n", - "list_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\n", - "get_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n", - "\n", - "print(list_tables_tool.invoke(\"\"))\n", - "\n", - "print(get_schema_tool.invoke(\"Artist\"))" - ] + "source": ["from langchain_community.agent_toolkits import SQLDatabaseToolkit\nfrom langchain_openai import ChatOpenAI\n\ntoolkit = SQLDatabaseToolkit(db=db, llm=ChatOpenAI(model=\"gpt-4o\"))\ntools = toolkit.get_tools()\n\nlist_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\nget_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n\nprint(list_tables_tool.invoke(\"\"))\n\nprint(get_schema_tool.invoke(\"Artist\"))"] }, { "cell_type": "markdown", @@ -381,25 +306,7 @@ ] } ], - "source": [ - "from langchain_core.tools import tool\n", - "\n", - "\n", - "@tool\n", - "def db_query_tool(query: str) -> str:\n", - " \"\"\"\n", - " Execute a SQL query against the database and get back the result.\n", - " If the query is not correct, an error message will be returned.\n", - " If an error is returned, rewrite the query, check the query, and try again.\n", - " \"\"\"\n", - " result = db.run_no_throw(query)\n", - " if not result:\n", - " return \"Error: Query failed. Please rewrite your query and try again.\"\n", - " return result\n", - "\n", - "\n", - "print(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))" - ] + "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef db_query_tool(query: str) -> str:\n \"\"\"\n Execute a SQL query against the database and get back the result.\n If the query is not correct, an error message will be returned.\n If an error is returned, rewrite the query, check the query, and try again.\n \"\"\"\n result = db.run_no_throw(query)\n if not result:\n return \"Error: Query failed. Please rewrite your query and try again.\"\n return result\n\n\nprint(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))"] }, { "cell_type": "markdown", @@ -440,33 +347,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_core.prompts import ChatPromptTemplate\n", - "\n", - "query_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\n", - "Double check the SQLite query for common mistakes, including:\n", - "- Using NOT IN with NULL values\n", - "- Using UNION when UNION ALL should have been used\n", - "- Using BETWEEN for exclusive ranges\n", - "- Data type mismatch in predicates\n", - "- Properly quoting identifiers\n", - "- Using the correct number of arguments for functions\n", - "- Casting to the correct data type\n", - "- Using the proper columns for joins\n", - "\n", - "If there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n", - "\n", - "You will call the appropriate tool to execute the query after running this check.\"\"\"\n", - "\n", - "query_check_prompt = ChatPromptTemplate.from_messages(\n", - " [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n", - ")\n", - "query_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n", - " [db_query_tool], tool_choice=\"required\"\n", - ")\n", - "\n", - "query_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})" - ] + "source": ["from langchain_core.prompts import ChatPromptTemplate\n\nquery_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\nDouble check the SQLite query for common mistakes, including:\n- Using NOT IN with NULL values\n- Using UNION when UNION ALL should have been used\n- Using BETWEEN for exclusive ranges\n- Data type mismatch in predicates\n- Properly quoting identifiers\n- Using the correct number of arguments for functions\n- Casting to the correct data type\n- Using the proper columns for joins\n\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n\nYou will call the appropriate tool to execute the query after running this check.\"\"\"\n\nquery_check_prompt = ChatPromptTemplate.from_messages(\n [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n)\nquery_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n [db_query_tool], tool_choice=\"required\"\n)\n\nquery_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})"] }, { "cell_type": "markdown", @@ -498,167 +379,7 @@ } }, "outputs": [], - "source": [ - "from typing import Annotated, Literal\n", - "\n", - "from langchain_core.messages import AIMessage\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "from langchain_openai import ChatOpenAI\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import END, StateGraph\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "# Define the state for the agent\n", - "class State(TypedDict):\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(State)\n", - "\n", - "\n", - "# Add a node for the first tool call\n", - "def first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n", - " return {\n", - " \"messages\": [\n", - " AIMessage(\n", - " content=\"\",\n", - " tool_calls=[\n", - " {\n", - " \"name\": \"sql_db_list_tables\",\n", - " \"args\": {},\n", - " \"id\": \"tool_abcd123\",\n", - " }\n", - " ],\n", - " )\n", - " ]\n", - " }\n", - "\n", - "\n", - "def model_check_query(state: State) -> dict[str, list[AIMessage]]:\n", - " \"\"\"\n", - " Use this tool to double-check if your query is correct before executing it.\n", - " \"\"\"\n", - " return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n", - "\n", - "\n", - "workflow.add_node(\"first_tool_call\", first_tool_call)\n", - "\n", - "# Add nodes for the first two tools\n", - "workflow.add_node(\n", - " \"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool])\n", - ")\n", - "workflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n", - "\n", - "# Add a node for a model to choose the relevant tables based on the question and available tables\n", - "model_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n", - " [get_schema_tool]\n", - ")\n", - "workflow.add_node(\n", - " \"model_get_schema\",\n", - " lambda state: {\n", - " \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n", - " },\n", - ")\n", - "\n", - "\n", - "# Describe a tool to represent the end state\n", - "class SubmitFinalAnswer(BaseModel):\n", - " \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n", - "\n", - " final_answer: str = Field(..., description=\"The final answer to the user\")\n", - "\n", - "\n", - "# Add a node for a model to generate a query based on the question and schema\n", - "query_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n", - "\n", - "Given an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n", - "\n", - "DO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n", - "\n", - "When generating the query:\n", - "\n", - "Output the SQL query that answers the input question without a tool call.\n", - "\n", - "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\n", - "You can order the results by a relevant column to return the most interesting examples in the database.\n", - "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n", - "\n", - "If you get an error while executing a query, rewrite the query and try again.\n", - "\n", - "If you get an empty result set, you should try to rewrite the query to get a non-empty result set. \n", - "NEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n", - "\n", - "If you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n", - "\n", - "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\n", - "query_gen_prompt = ChatPromptTemplate.from_messages(\n", - " [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n", - ")\n", - "query_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n", - " [SubmitFinalAnswer]\n", - ")\n", - "\n", - "\n", - "def query_gen_node(state: State):\n", - " message = query_gen.invoke(state)\n", - "\n", - " # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n", - " tool_messages = []\n", - " if message.tool_calls:\n", - " for tc in message.tool_calls:\n", - " if tc[\"name\"] != \"SubmitFinalAnswer\":\n", - " tool_messages.append(\n", - " ToolMessage(\n", - " content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",\n", - " tool_call_id=tc[\"id\"],\n", - " )\n", - " )\n", - " else:\n", - " tool_messages = []\n", - " return {\"messages\": [message] + tool_messages}\n", - "\n", - "\n", - "workflow.add_node(\"query_gen\", query_gen_node)\n", - "\n", - "# Add a node for the model to check the query before executing it\n", - "workflow.add_node(\"correct_query\", model_check_query)\n", - "\n", - "# Add node for executing the query\n", - "workflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))\n", - "\n", - "\n", - "# Define a conditional edge to decide whether to continue or end the workflow\n", - "def should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is a tool call, then we finish\n", - " if getattr(last_message, \"tool_calls\", None):\n", - " return END\n", - " if last_message.content.startswith(\"Error:\"):\n", - " return \"query_gen\"\n", - " else:\n", - " return \"correct_query\"\n", - "\n", - "\n", - "# Specify the edges between the nodes\n", - "workflow.set_entry_point(\"first_tool_call\")\n", - "workflow.add_edge(\"first_tool_call\", \"list_tables_tool\")\n", - "workflow.add_edge(\"list_tables_tool\", \"model_get_schema\")\n", - "workflow.add_edge(\"model_get_schema\", \"get_schema_tool\")\n", - "workflow.add_edge(\"get_schema_tool\", \"query_gen\")\n", - "workflow.add_conditional_edges(\n", - " \"query_gen\",\n", - " should_continue,\n", - ")\n", - "workflow.add_edge(\"correct_query\", \"execute_query\")\n", - "workflow.add_edge(\"execute_query\", \"query_gen\")\n", - "\n", - "# Compile the workflow into a runnable\n", - "app = workflow.compile()" - ] + "source": ["from typing import Annotated, Literal\n\nfrom langchain_core.messages import AIMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\n# Define the state for the agent\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Add a node for the first tool call\ndef first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n return {\n \"messages\": [\n AIMessage(\n content=\"\",\n tool_calls=[\n {\n \"name\": \"sql_db_list_tables\",\n \"args\": {},\n \"id\": \"tool_abcd123\",\n }\n ],\n )\n ]\n }\n\n\ndef model_check_query(state: State) -> dict[str, list[AIMessage]]:\n \"\"\"\n Use this tool to double-check if your query is correct before executing it.\n \"\"\"\n return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n\n\nworkflow.add_node(\"first_tool_call\", first_tool_call)\n\n# Add nodes for the first two tools\nworkflow.add_node(\n \"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool])\n)\nworkflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n\n# Add a node for a model to choose the relevant tables based on the question and available tables\nmodel_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n [get_schema_tool]\n)\nworkflow.add_node(\n \"model_get_schema\",\n lambda state: {\n \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n },\n)\n\n\n# Describe a tool to represent the end state\nclass SubmitFinalAnswer(BaseModel):\n \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n\n final_answer: str = Field(..., description=\"The final answer to the user\")\n\n\n# Add a node for a model to generate a query based on the question and schema\nquery_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n\nGiven an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n\nDO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n\nWhen generating the query:\n\nOutput the SQL query that answers the input question without a tool call.\n\nUnless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\nYou can order the results by a relevant column to return the most interesting examples in the database.\nNever query for all the columns from a specific table, only ask for the relevant columns given the question.\n\nIf you get an error while executing a query, rewrite the query and try again.\n\nIf you get an empty result set, you should try to rewrite the query to get a non-empty result set. \nNEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n\nIf you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n\nDO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\nquery_gen_prompt = ChatPromptTemplate.from_messages(\n [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n)\nquery_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n [SubmitFinalAnswer]\n)\n\n\ndef query_gen_node(state: State):\n message = query_gen.invoke(state)\n\n # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n tool_messages = []\n if message.tool_calls:\n for tc in message.tool_calls:\n if tc[\"name\"] != \"SubmitFinalAnswer\":\n tool_messages.append(\n ToolMessage(\n content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",\n tool_call_id=tc[\"id\"],\n )\n )\n else:\n tool_messages = []\n return {\"messages\": [message] + tool_messages}\n\n\nworkflow.add_node(\"query_gen\", query_gen_node)\n\n# Add a node for the model to check the query before executing it\nworkflow.add_node(\"correct_query\", model_check_query)\n\n# Add node for executing the query\nworkflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))\n\n\n# Define a conditional edge to decide whether to continue or end the workflow\ndef should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is a tool call, then we finish\n if getattr(last_message, \"tool_calls\", None):\n return END\n if last_message.content.startswith(\"Error:\"):\n return \"query_gen\"\n else:\n return \"correct_query\"\n\n\n# Specify the edges between the nodes\nworkflow.add_edge(START, \"first_tool_call\")\nworkflow.add_edge(\"first_tool_call\", \"list_tables_tool\")\nworkflow.add_edge(\"list_tables_tool\", \"model_get_schema\")\nworkflow.add_edge(\"model_get_schema\", \"get_schema_tool\")\nworkflow.add_edge(\"get_schema_tool\", \"query_gen\")\nworkflow.add_conditional_edges(\n \"query_gen\",\n should_continue,\n)\nworkflow.add_edge(\"correct_query\", \"execute_query\")\nworkflow.add_edge(\"execute_query\", \"query_gen\")\n\n# Compile the workflow into a runnable\napp = workflow.compile()"] }, { "cell_type": "markdown", @@ -699,18 +420,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "from langchain_core.runnables.graph import MermaidDrawMethod\n", - "\n", - "display(\n", - " Image(\n", - " app.get_graph().draw_mermaid_png(\n", - " draw_method=MermaidDrawMethod.API,\n", - " )\n", - " )\n", - ")" - ] + "source": ["from IPython.display import Image, display\nfrom langchain_core.runnables.graph import MermaidDrawMethod\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n draw_method=MermaidDrawMethod.API,\n )\n )\n)"] }, { "cell_type": "markdown", @@ -742,17 +452,7 @@ "output_type": "execute_result" } ], - "source": [ - "import json\n", - "\n", - "messages = app.invoke(\n", - " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", - ")\n", - "json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - "]\n", - "json.loads(json_str)[\"final_answer\"]" - ] + "source": ["import json\n\nmessages = app.invoke(\n {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n)\njson_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n \"arguments\"\n]\njson.loads(json_str)[\"final_answer\"]"] }, { "cell_type": "code", @@ -760,12 +460,7 @@ "id": "3bf7709f-500c-4f28-bb85-dda317286c63", "metadata": {}, "outputs": [], - "source": [ - "for event in app.stream(\n", - " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", - "):\n", - " print(event)" - ] + "source": ["for event in app.stream(\n {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n):\n print(event)"] }, { "attachments": { @@ -798,20 +493,7 @@ "id": "a80f4adc-a8dc-403c-9bef-6de5e873b9bc", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "\n", - "\n", - "def predict_sql_agent_answer(example: dict):\n", - " \"\"\"Use this for answer evaluation\"\"\"\n", - " msg = {\"messages\": (\"user\", example[\"input\"])}\n", - " messages = app.invoke(msg)\n", - " json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - " ]\n", - " response = json.loads(json_str)[\"final_answer\"]\n", - " return {\"response\": response}" - ] + "source": ["import json\n\n\ndef predict_sql_agent_answer(example: dict):\n \"\"\"Use this for answer evaluation\"\"\"\n msg = {\"messages\": (\"user\", example[\"input\"])}\n messages = app.invoke(msg)\n json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n \"arguments\"\n ]\n response = json.loads(json_str)[\"final_answer\"]\n return {\"response\": response}"] }, { "cell_type": "code", @@ -819,42 +501,7 @@ "id": "1040233f-3751-4bd3-902f-709fc2e1ecf5", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain_openai import ChatOpenAI\n", - "\n", - "# Grade prompt\n", - "grade_prompt_answer_accuracy = prompt = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n", - "\n", - "\n", - "def answer_evaluator(run, example) -> dict:\n", - " \"\"\"\n", - " A simple evaluator for RAG answer accuracy\n", - " \"\"\"\n", - "\n", - " # Get question, ground truth answer, chain\n", - " input_question = example.inputs[\"input\"]\n", - " reference = example.outputs[\"output\"]\n", - " prediction = run.outputs[\"response\"]\n", - "\n", - " # LLM grader\n", - " llm = ChatOpenAI(model=\"gpt-4-turbo\", temperature=0)\n", - "\n", - " # Structured prompt\n", - " answer_grader = grade_prompt_answer_accuracy | llm\n", - "\n", - " # Run evaluator\n", - " score = answer_grader.invoke(\n", - " {\n", - " \"question\": input_question,\n", - " \"correct_answer\": reference,\n", - " \"student_answer\": prediction,\n", - " }\n", - " )\n", - " score = score[\"Score\"]\n", - "\n", - " return {\"key\": \"answer_v_reference_score\", \"score\": score}" - ] + "source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\n# Grade prompt\ngrade_prompt_answer_accuracy = prompt = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n\n\ndef answer_evaluator(run, example) -> dict:\n \"\"\"\n A simple evaluator for RAG answer accuracy\n \"\"\"\n\n # Get question, ground truth answer, chain\n input_question = example.inputs[\"input\"]\n reference = example.outputs[\"output\"]\n prediction = run.outputs[\"response\"]\n\n # LLM grader\n llm = ChatOpenAI(model=\"gpt-4-turbo\", temperature=0)\n\n # Structured prompt\n answer_grader = grade_prompt_answer_accuracy | llm\n\n # Run evaluator\n score = answer_grader.invoke(\n {\n \"question\": input_question,\n \"correct_answer\": reference,\n \"student_answer\": prediction,\n }\n )\n score = score[\"Score\"]\n\n return {\"key\": \"answer_v_reference_score\", \"score\": score}"] }, { "cell_type": "code", @@ -862,19 +509,7 @@ "id": "eb814b85-70ba-4699-9038-20266b53efbd", "metadata": {}, "outputs": [], - "source": [ - "from langsmith.evaluation import evaluate\n", - "\n", - "dataset_name = \"SQL Agent Response\"\n", - "experiment_results = evaluate(\n", - " predict_sql_agent_answer,\n", - " data=dataset_name,\n", - " evaluators=[answer_evaluator],\n", - " num_repetitions=3,\n", - " experiment_prefix=\"sql-agent-multi-step-response-v-reference\",\n", - " metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n", - ")" - ] + "source": ["from langsmith.evaluation import evaluate\n\ndataset_name = \"SQL Agent Response\"\nexperiment_results = evaluate(\n predict_sql_agent_answer,\n data=dataset_name,\n evaluators=[answer_evaluator],\n num_repetitions=3,\n experiment_prefix=\"sql-agent-multi-step-response-v-reference\",\n metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n)"] }, { "attachments": { @@ -909,15 +544,7 @@ "id": "ef84d2f7-fa52-46ca-8939-616e5ac4d101", "metadata": {}, "outputs": [], - "source": [ - "# These are the tools that we expect the agent to use\n", - "expected_trajectory = [\n", - " \"sql_db_list_tables\", # first: list_tables_tool node\n", - " \"sql_db_schema\", # second: get_schema_tool node\n", - " \"db_query_tool\", # third: execute_query node\n", - " \"SubmitFinalAnswer\",\n", - "] # fourth: query_gen" - ] + "source": ["# These are the tools that we expect the agent to use\nexpected_trajectory = [\n \"sql_db_list_tables\", # first: list_tables_tool node\n \"sql_db_schema\", # second: get_schema_tool node\n \"db_query_tool\", # third: execute_query node\n \"SubmitFinalAnswer\",\n] # fourth: query_gen"] }, { "cell_type": "code", @@ -925,13 +552,7 @@ "id": "1b7b007a-1dd2-4f3e-b157-b9d7ec2ea0a2", "metadata": {}, "outputs": [], - "source": [ - "def predict_sql_agent_messages(example: dict):\n", - " \"\"\"Use this for answer evaluation\"\"\"\n", - " msg = {\"messages\": (\"user\", example[\"input\"])}\n", - " messages = app.invoke(msg)\n", - " return {\"response\": messages}" - ] + "source": ["def predict_sql_agent_messages(example: dict):\n \"\"\"Use this for answer evaluation\"\"\"\n msg = {\"messages\": (\"user\", example[\"input\"])}\n messages = app.invoke(msg)\n return {\"response\": messages}"] }, { "cell_type": "code", @@ -939,67 +560,7 @@ "id": "ae2fe538-1c6d-4186-80dd-1d240d253f40", "metadata": {}, "outputs": [], - "source": [ - "from langsmith.schemas import Example, Run\n", - "\n", - "\n", - "def find_tool_calls(messages):\n", - " \"\"\"\n", - " Find all tool calls in the messages returned\n", - " \"\"\"\n", - " tool_calls = [\n", - " tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n", - " ]\n", - " return tool_calls\n", - "\n", - "\n", - "def contains_all_tool_calls_in_order_exact_match(\n", - " root_run: Run, example: Example\n", - ") -> dict:\n", - " \"\"\"\n", - " Check if all expected tools are called in exact order and without any additional tool calls.\n", - " \"\"\"\n", - " expected_trajectory = [\n", - " \"sql_db_list_tables\",\n", - " \"sql_db_schema\",\n", - " \"db_query_tool\",\n", - " \"SubmitFinalAnswer\",\n", - " ]\n", - " messages = root_run.outputs[\"response\"]\n", - " tool_calls = find_tool_calls(messages)\n", - "\n", - " # Print the tool calls for debugging\n", - " print(\"Here are my tool calls:\")\n", - " print(tool_calls)\n", - "\n", - " # Check if the tool calls match the expected trajectory exactly\n", - " if tool_calls == expected_trajectory:\n", - " score = 1\n", - " else:\n", - " score = 0\n", - "\n", - " return {\"score\": int(score), \"key\": \"multi_tool_call_in_exact_order\"}\n", - "\n", - "\n", - "def contains_all_tool_calls_in_order(root_run: Run, example: Example) -> dict:\n", - " \"\"\"\n", - " Check if all expected tools are called in order,\n", - " but it allows for other tools to be called in between the expected ones.\n", - " \"\"\"\n", - " messages = root_run.outputs[\"response\"]\n", - " tool_calls = find_tool_calls(messages)\n", - "\n", - " # Print the tool calls for debugging\n", - " print(\"Here are my tool calls:\")\n", - " print(tool_calls)\n", - "\n", - " it = iter(tool_calls)\n", - " if all(elem in it for elem in expected_trajectory):\n", - " score = 1\n", - " else:\n", - " score = 0\n", - " return {\"score\": int(score), \"key\": \"multi_tool_call_in_order\"}" - ] + "source": ["from langsmith.schemas import Example, Run\n\n\ndef find_tool_calls(messages):\n \"\"\"\n Find all tool calls in the messages returned\n \"\"\"\n tool_calls = [\n tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n ]\n return tool_calls\n\n\ndef contains_all_tool_calls_in_order_exact_match(\n root_run: Run, example: Example\n) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n expected_trajectory = [\n \"sql_db_list_tables\",\n \"sql_db_schema\",\n \"db_query_tool\",\n \"SubmitFinalAnswer\",\n ]\n messages = root_run.outputs[\"response\"]\n tool_calls = find_tool_calls(messages)\n\n # Print the tool calls for debugging\n print(\"Here are my tool calls:\")\n print(tool_calls)\n\n # Check if the tool calls match the expected trajectory exactly\n if tool_calls == expected_trajectory:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"multi_tool_call_in_exact_order\"}\n\n\ndef contains_all_tool_calls_in_order(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in order,\n but it allows for other tools to be called in between the expected ones.\n \"\"\"\n messages = root_run.outputs[\"response\"]\n tool_calls = find_tool_calls(messages)\n\n # Print the tool calls for debugging\n print(\"Here are my tool calls:\")\n print(tool_calls)\n\n it = iter(tool_calls)\n if all(elem in it for elem in expected_trajectory):\n score = 1\n else:\n score = 0\n return {\"score\": int(score), \"key\": \"multi_tool_call_in_order\"}"] }, { "cell_type": "code", @@ -1007,19 +568,7 @@ "id": "cf02e843-438d-4168-a27a-8f1e0266f8d7", "metadata": {}, "outputs": [], - "source": [ - "experiment_results = evaluate(\n", - " predict_sql_agent_messages,\n", - " data=dataset_name,\n", - " evaluators=[\n", - " contains_all_tool_calls_in_order,\n", - " contains_all_tool_calls_in_order_exact_match,\n", - " ],\n", - " num_repetitions=3,\n", - " experiment_prefix=\"sql-agent-multi-step-tool-calling-trajecory-in-order\",\n", - " metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n", - ")" - ] + "source": ["experiment_results = evaluate(\n predict_sql_agent_messages,\n data=dataset_name,\n evaluators=[\n contains_all_tool_calls_in_order,\n contains_all_tool_calls_in_order_exact_match,\n ],\n num_repetitions=3,\n experiment_prefix=\"sql-agent-multi-step-tool-calling-trajecory-in-order\",\n metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n)"] }, { "attachments": { @@ -1060,7 +609,7 @@ "id": "0681b6e0-196e-440c-ab16-1a530411719e", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/tutorials/tnt-llm/tnt-llm.ipynb b/examples/tutorials/tnt-llm/tnt-llm.ipynb index e92e99a5d..3dc2fb50b 100644 --- a/examples/tutorials/tnt-llm/tnt-llm.ipynb +++ b/examples/tutorials/tnt-llm/tnt-llm.ipynb @@ -38,12 +38,7 @@ "id": "abd95235-4da5-4d6a-985f-78b2572ad626", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langchain_anthropic langsmith\n", - "# For the embedding-based classifier use in phase 2\n", - "%pip install -U sklearn langchain_openai" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain_anthropic langsmith\n# For the embedding-based classifier use in phase 2\n%pip install -U sklearn langchain_openai"] }, { "cell_type": "code", @@ -51,20 +46,7 @@ "id": "d98b62e4-d327-4442-8482-65529500a8a7", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "from getpass import getpass\n", - "\n", - "if \"ANTHROPIC_API_KEY\" not in os.environ:\n", - " os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"Enter your ANTHROPIC_API_KEY: \")\n", - "\n", - "# (Optional) Enable tracing\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", - "os.environ[\"LANGCHAIN_PROJECT\"] = \"tnt-llm\"\n", - "\n", - "if \"LANGCHAIN_API_KEY\" not in os.environ:\n", - " os.environ[\"LANGCHAIN_API_KEY\"] = getpass(\"Enter your LANGCHAIN_API_KEY: \")" - ] + "source": ["import os\nfrom getpass import getpass\n\nif \"ANTHROPIC_API_KEY\" not in os.environ:\n os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"Enter your ANTHROPIC_API_KEY: \")\n\n# (Optional) Enable tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"tnt-llm\"\n\nif \"LANGCHAIN_API_KEY\" not in os.environ:\n os.environ[\"LANGCHAIN_API_KEY\"] = getpass(\"Enter your LANGCHAIN_API_KEY: \")"] }, { "cell_type": "markdown", @@ -84,31 +66,7 @@ "id": "580d82b5-b60c-47a4-9c8b-e28be22ca0e3", "metadata": {}, "outputs": [], - "source": [ - "import logging\n", - "import operator\n", - "from typing import Annotated, List, Optional, TypedDict\n", - "\n", - "logging.basicConfig(level=logging.WARNING)\n", - "logger = logging.getLogger(\"tnt-llm\")\n", - "\n", - "\n", - "class Doc(TypedDict):\n", - " id: str\n", - " content: str\n", - " summary: Optional[str]\n", - " explanation: Optional[str]\n", - " category: Optional[str]\n", - "\n", - "\n", - "class TaxonomyGenerationState(TypedDict):\n", - " # The raw docs; we inject summaries within them in the first step\n", - " documents: List[Doc]\n", - " # Indices to be concise\n", - " minibatches: List[List[int]]\n", - " # Candidate Taxonomies (full trajectory)\n", - " clusters: Annotated[List[List[dict]], operator.add]" - ] + "source": ["import logging\nimport operator\nfrom typing import Annotated, List, Optional, TypedDict\n\nlogging.basicConfig(level=logging.WARNING)\nlogger = logging.getLogger(\"tnt-llm\")\n\n\nclass Doc(TypedDict):\n id: str\n content: str\n summary: Optional[str]\n explanation: Optional[str]\n category: Optional[str]\n\n\nclass TaxonomyGenerationState(TypedDict):\n # The raw docs; we inject summaries within them in the first step\n documents: List[Doc]\n # Indices to be concise\n minibatches: List[List[int]]\n # Candidate Taxonomies (full trajectory)\n clusters: Annotated[List[List[dict]], operator.add]"] }, { "cell_type": "markdown", @@ -126,77 +84,7 @@ "id": "ff02c2a1-18b5-4848-96bb-27ff00978570", "metadata": {}, "outputs": [], - "source": [ - "import re\n", - "\n", - "from langchain import hub\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough\n", - "\n", - "summary_prompt = hub.pull(\"wfh/tnt-llm-summary-generation\").partial(\n", - " summary_length=20, explanation_length=30\n", - ")\n", - "\n", - "\n", - "def parse_summary(xml_string: str) -> dict:\n", - " summary_pattern = r\"(.*?)\"\n", - " explanation_pattern = r\"(.*?)\"\n", - "\n", - " summary_match = re.search(summary_pattern, xml_string, re.DOTALL)\n", - " explanation_match = re.search(explanation_pattern, xml_string, re.DOTALL)\n", - "\n", - " summary = summary_match.group(1).strip() if summary_match else \"\"\n", - " explanation = explanation_match.group(1).strip() if explanation_match else \"\"\n", - "\n", - " return {\"summary\": summary, \"explanation\": explanation}\n", - "\n", - "\n", - "summary_llm_chain = (\n", - " summary_prompt\n", - " | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - " | StrOutputParser()\n", - " # Customize the tracing name for easier organization\n", - ").with_config(run_name=\"GenerateSummary\")\n", - "summary_chain = summary_llm_chain | parse_summary\n", - "\n", - "\n", - "# Now combine as a \"map\" operation in a map-reduce chain\n", - "# Input: state\n", - "# Output: state U summaries\n", - "# Processes docs in parallel\n", - "def get_content(state: TaxonomyGenerationState):\n", - " docs = state[\"documents\"]\n", - " return [{\"content\": doc[\"content\"]} for doc in docs]\n", - "\n", - "\n", - "map_step = RunnablePassthrough.assign(\n", - " summaries=get_content\n", - " # This effectively creates a \"map\" operation\n", - " # Note you can make this more robust by handling individual errors\n", - " | RunnableLambda(func=summary_chain.batch, afunc=summary_chain.abatch)\n", - ")\n", - "\n", - "\n", - "def reduce_summaries(combined: dict) -> TaxonomyGenerationState:\n", - " summaries = combined[\"summaries\"]\n", - " documents = combined[\"documents\"]\n", - " return {\n", - " \"documents\": [\n", - " {\n", - " \"id\": doc[\"id\"],\n", - " \"content\": doc[\"content\"],\n", - " \"summary\": summ_info[\"summary\"],\n", - " \"explanation\": summ_info[\"explanation\"],\n", - " }\n", - " for doc, summ_info in zip(documents, summaries)\n", - " ]\n", - " }\n", - "\n", - "\n", - "# This is actually the node itself!\n", - "map_reduce_chain = map_step | reduce_summaries" - ] + "source": ["import re\n\nfrom langchain import hub\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough\n\nsummary_prompt = hub.pull(\"wfh/tnt-llm-summary-generation\").partial(\n summary_length=20, explanation_length=30\n)\n\n\ndef parse_summary(xml_string: str) -> dict:\n summary_pattern = r\"(.*?)\"\n explanation_pattern = r\"(.*?)\"\n\n summary_match = re.search(summary_pattern, xml_string, re.DOTALL)\n explanation_match = re.search(explanation_pattern, xml_string, re.DOTALL)\n\n summary = summary_match.group(1).strip() if summary_match else \"\"\n explanation = explanation_match.group(1).strip() if explanation_match else \"\"\n\n return {\"summary\": summary, \"explanation\": explanation}\n\n\nsummary_llm_chain = (\n summary_prompt\n | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n | StrOutputParser()\n # Customize the tracing name for easier organization\n).with_config(run_name=\"GenerateSummary\")\nsummary_chain = summary_llm_chain | parse_summary\n\n\n# Now combine as a \"map\" operation in a map-reduce chain\n# Input: state\n# Output: state U summaries\n# Processes docs in parallel\ndef get_content(state: TaxonomyGenerationState):\n docs = state[\"documents\"]\n return [{\"content\": doc[\"content\"]} for doc in docs]\n\n\nmap_step = RunnablePassthrough.assign(\n summaries=get_content\n # This effectively creates a \"map\" operation\n # Note you can make this more robust by handling individual errors\n | RunnableLambda(func=summary_chain.batch, afunc=summary_chain.abatch)\n)\n\n\ndef reduce_summaries(combined: dict) -> TaxonomyGenerationState:\n summaries = combined[\"summaries\"]\n documents = combined[\"documents\"]\n return {\n \"documents\": [\n {\n \"id\": doc[\"id\"],\n \"content\": doc[\"content\"],\n \"summary\": summ_info[\"summary\"],\n \"explanation\": summ_info[\"explanation\"],\n }\n for doc, summ_info in zip(documents, summaries)\n ]\n }\n\n\n# This is actually the node itself!\nmap_reduce_chain = map_step | reduce_summaries"] }, { "cell_type": "markdown", @@ -214,36 +102,7 @@ "id": "3e0139c3-b5ba-42b9-9367-33533d66eb58", "metadata": {}, "outputs": [], - "source": [ - "import random\n", - "\n", - "\n", - "def get_minibatches(state: TaxonomyGenerationState, config: RunnableConfig):\n", - " batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n", - " original = state[\"documents\"]\n", - " indices = list(range(len(original)))\n", - " random.shuffle(indices)\n", - " if len(indices) < batch_size:\n", - " # Don't pad needlessly if we can't fill a single batch\n", - " return [indices]\n", - "\n", - " num_full_batches = len(indices) // batch_size\n", - "\n", - " batches = [\n", - " indices[i * batch_size : (i + 1) * batch_size] for i in range(num_full_batches)\n", - " ]\n", - "\n", - " leftovers = len(indices) % batch_size\n", - " if leftovers:\n", - " last_batch = indices[num_full_batches * batch_size :]\n", - " elements_to_add = batch_size - leftovers\n", - " last_batch += random.sample(indices, elements_to_add)\n", - " batches.append(last_batch)\n", - "\n", - " return {\n", - " \"minibatches\": batches,\n", - " }" - ] + "source": ["import random\n\n\ndef get_minibatches(state: TaxonomyGenerationState, config: RunnableConfig):\n batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n original = state[\"documents\"]\n indices = list(range(len(original)))\n random.shuffle(indices)\n if len(indices) < batch_size:\n # Don't pad needlessly if we can't fill a single batch\n return [indices]\n\n num_full_batches = len(indices) // batch_size\n\n batches = [\n indices[i * batch_size : (i + 1) * batch_size] for i in range(num_full_batches)\n ]\n\n leftovers = len(indices) % batch_size\n if leftovers:\n last_batch = indices[num_full_batches * batch_size :]\n elements_to_add = batch_size - leftovers\n last_batch += random.sample(indices, elements_to_add)\n batches.append(last_batch)\n\n return {\n \"minibatches\": batches,\n }"] }, { "cell_type": "markdown", @@ -261,80 +120,7 @@ "id": "224ed013-2963-489c-b734-315cad701d59", "metadata": {}, "outputs": [], - "source": [ - "from typing import Dict\n", - "\n", - "from langchain_core.runnables import Runnable\n", - "\n", - "\n", - "def parse_taxa(output_text: str) -> Dict:\n", - " \"\"\"Extract the taxonomy from the generated output.\"\"\"\n", - " cluster_matches = re.findall(\n", - " r\"\\s*(.*?)\\s*(.*?)\\s*(.*?)\\s*\",\n", - " output_text,\n", - " re.DOTALL,\n", - " )\n", - " clusters = [\n", - " {\"id\": id.strip(), \"name\": name.strip(), \"description\": description.strip()}\n", - " for id, name, description in cluster_matches\n", - " ]\n", - " # We don't parse the explanation since it isn't used downstream\n", - " return {\"clusters\": clusters}\n", - "\n", - "\n", - "def format_docs(docs: List[Doc]) -> str:\n", - " xml_table = \"\\n\"\n", - " for doc in docs:\n", - " xml_table += f'{doc[\"summary\"]}\\n'\n", - " xml_table += \"\"\n", - " return xml_table\n", - "\n", - "\n", - "def format_taxonomy(clusters):\n", - " xml = \"\\n\"\n", - " for label in clusters:\n", - " xml += \" \\n\"\n", - " xml += f' {label[\"id\"]}\\n'\n", - " xml += f' {label[\"name\"]}\\n'\n", - " xml += f' {label[\"description\"]}\\n'\n", - " xml += \" \\n\"\n", - " xml += \"\"\n", - " return xml\n", - "\n", - "\n", - "def invoke_taxonomy_chain(\n", - " chain: Runnable,\n", - " state: TaxonomyGenerationState,\n", - " config: RunnableConfig,\n", - " mb_indices: List[int],\n", - ") -> TaxonomyGenerationState:\n", - " configurable = config[\"configurable\"]\n", - " docs = state[\"documents\"]\n", - " minibatch = [docs[idx] for idx in mb_indices]\n", - " data_table_xml = format_docs(minibatch)\n", - "\n", - " previous_taxonomy = state[\"clusters\"][-1] if state[\"clusters\"] else []\n", - " cluster_table_xml = format_taxonomy(previous_taxonomy)\n", - "\n", - " updated_taxonomy = chain.invoke(\n", - " {\n", - " \"data_xml\": data_table_xml,\n", - " \"use_case\": configurable[\"use_case\"],\n", - " \"cluster_table_xml\": cluster_table_xml,\n", - " \"suggestion_length\": configurable.get(\"suggestion_length\", 30),\n", - " \"cluster_name_length\": configurable.get(\"cluster_name_length\", 10),\n", - " \"cluster_description_length\": configurable.get(\n", - " \"cluster_description_length\", 30\n", - " ),\n", - " \"explanation_length\": configurable.get(\"explanation_length\", 20),\n", - " \"max_num_clusters\": configurable.get(\"max_num_clusters\", 25),\n", - " }\n", - " )\n", - "\n", - " return {\n", - " \"clusters\": [updated_taxonomy[\"clusters\"]],\n", - " }" - ] + "source": ["from typing import Dict\n\nfrom langchain_core.runnables import Runnable\n\n\ndef parse_taxa(output_text: str) -> Dict:\n \"\"\"Extract the taxonomy from the generated output.\"\"\"\n cluster_matches = re.findall(\n r\"\\s*(.*?)\\s*(.*?)\\s*(.*?)\\s*\",\n output_text,\n re.DOTALL,\n )\n clusters = [\n {\"id\": id.strip(), \"name\": name.strip(), \"description\": description.strip()}\n for id, name, description in cluster_matches\n ]\n # We don't parse the explanation since it isn't used downstream\n return {\"clusters\": clusters}\n\n\ndef format_docs(docs: List[Doc]) -> str:\n xml_table = \"\\n\"\n for doc in docs:\n xml_table += f'{doc[\"summary\"]}\\n'\n xml_table += \"\"\n return xml_table\n\n\ndef format_taxonomy(clusters):\n xml = \"\\n\"\n for label in clusters:\n xml += \" \\n\"\n xml += f' {label[\"id\"]}\\n'\n xml += f' {label[\"name\"]}\\n'\n xml += f' {label[\"description\"]}\\n'\n xml += \" \\n\"\n xml += \"\"\n return xml\n\n\ndef invoke_taxonomy_chain(\n chain: Runnable,\n state: TaxonomyGenerationState,\n config: RunnableConfig,\n mb_indices: List[int],\n) -> TaxonomyGenerationState:\n configurable = config[\"configurable\"]\n docs = state[\"documents\"]\n minibatch = [docs[idx] for idx in mb_indices]\n data_table_xml = format_docs(minibatch)\n\n previous_taxonomy = state[\"clusters\"][-1] if state[\"clusters\"] else []\n cluster_table_xml = format_taxonomy(previous_taxonomy)\n\n updated_taxonomy = chain.invoke(\n {\n \"data_xml\": data_table_xml,\n \"use_case\": configurable[\"use_case\"],\n \"cluster_table_xml\": cluster_table_xml,\n \"suggestion_length\": configurable.get(\"suggestion_length\", 30),\n \"cluster_name_length\": configurable.get(\"cluster_name_length\", 10),\n \"cluster_description_length\": configurable.get(\n \"cluster_description_length\", 30\n ),\n \"explanation_length\": configurable.get(\"explanation_length\", 20),\n \"max_num_clusters\": configurable.get(\"max_num_clusters\", 25),\n }\n )\n\n return {\n \"clusters\": [updated_taxonomy[\"clusters\"]],\n }"] }, { "cell_type": "markdown", @@ -350,34 +136,7 @@ "id": "553dff30-ce53-47d8-ab3c-d2f437b7d5f4", "metadata": {}, "outputs": [], - "source": [ - "# We will share an LLM for each step of the generate -> update -> review cycle\n", - "# You may want to consider using Opus or another more powerful model for this\n", - "taxonomy_generation_llm = ChatAnthropic(\n", - " model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000\n", - ")\n", - "\n", - "\n", - "## Initial generation\n", - "taxonomy_generation_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-generation\").partial(\n", - " use_case=\"Generate the taxonomy that can be used to label the user intent in the conversation.\",\n", - ")\n", - "\n", - "taxa_gen_llm_chain = (\n", - " taxonomy_generation_prompt | taxonomy_generation_llm | StrOutputParser()\n", - ").with_config(run_name=\"GenerateTaxonomy\")\n", - "\n", - "\n", - "generate_taxonomy_chain = taxa_gen_llm_chain | parse_taxa\n", - "\n", - "\n", - "def generate_taxonomy(\n", - " state: TaxonomyGenerationState, config: RunnableConfig\n", - ") -> TaxonomyGenerationState:\n", - " return invoke_taxonomy_chain(\n", - " generate_taxonomy_chain, state, config, state[\"minibatches\"][0]\n", - " )" - ] + "source": ["# We will share an LLM for each step of the generate -> update -> review cycle\n# You may want to consider using Opus or another more powerful model for this\ntaxonomy_generation_llm = ChatAnthropic(\n model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000\n)\n\n\n## Initial generation\ntaxonomy_generation_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-generation\").partial(\n use_case=\"Generate the taxonomy that can be used to label the user intent in the conversation.\",\n)\n\ntaxa_gen_llm_chain = (\n taxonomy_generation_prompt | taxonomy_generation_llm | StrOutputParser()\n).with_config(run_name=\"GenerateTaxonomy\")\n\n\ngenerate_taxonomy_chain = taxa_gen_llm_chain | parse_taxa\n\n\ndef generate_taxonomy(\n state: TaxonomyGenerationState, config: RunnableConfig\n) -> TaxonomyGenerationState:\n return invoke_taxonomy_chain(\n generate_taxonomy_chain, state, config, state[\"minibatches\"][0]\n )"] }, { "cell_type": "markdown", @@ -395,25 +154,7 @@ "id": "b8739b5b-ba8a-4c40-bd25-a3b06a19949d", "metadata": {}, "outputs": [], - "source": [ - "taxonomy_update_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-update\")\n", - "\n", - "taxa_update_llm_chain = (\n", - " taxonomy_update_prompt | taxonomy_generation_llm | StrOutputParser()\n", - ").with_config(run_name=\"UpdateTaxonomy\")\n", - "\n", - "\n", - "update_taxonomy_chain = taxa_update_llm_chain | parse_taxa\n", - "\n", - "\n", - "def update_taxonomy(\n", - " state: TaxonomyGenerationState, config: RunnableConfig\n", - ") -> TaxonomyGenerationState:\n", - " which_mb = len(state[\"clusters\"]) % len(state[\"minibatches\"])\n", - " return invoke_taxonomy_chain(\n", - " update_taxonomy_chain, state, config, state[\"minibatches\"][which_mb]\n", - " )" - ] + "source": ["taxonomy_update_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-update\")\n\ntaxa_update_llm_chain = (\n taxonomy_update_prompt | taxonomy_generation_llm | StrOutputParser()\n).with_config(run_name=\"UpdateTaxonomy\")\n\n\nupdate_taxonomy_chain = taxa_update_llm_chain | parse_taxa\n\n\ndef update_taxonomy(\n state: TaxonomyGenerationState, config: RunnableConfig\n) -> TaxonomyGenerationState:\n which_mb = len(state[\"clusters\"]) % len(state[\"minibatches\"])\n return invoke_taxonomy_chain(\n update_taxonomy_chain, state, config, state[\"minibatches\"][which_mb]\n )"] }, { "cell_type": "markdown", @@ -431,28 +172,7 @@ "id": "0039cf1c-54d5-4e9e-8dd6-a5cebfaec92d", "metadata": {}, "outputs": [], - "source": [ - "taxonomy_review_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-review\")\n", - "\n", - "taxa_review_llm_chain = (\n", - " taxonomy_review_prompt | taxonomy_generation_llm | StrOutputParser()\n", - ").with_config(run_name=\"ReviewTaxonomy\")\n", - "\n", - "\n", - "review_taxonomy_chain = taxa_review_llm_chain | parse_taxa\n", - "\n", - "\n", - "def review_taxonomy(\n", - " state: TaxonomyGenerationState, config: RunnableConfig\n", - ") -> TaxonomyGenerationState:\n", - " batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n", - " original = state[\"documents\"]\n", - " indices = list(range(len(original)))\n", - " random.shuffle(indices)\n", - " return invoke_taxonomy_chain(\n", - " review_taxonomy_chain, state, config, indices[:batch_size]\n", - " )" - ] + "source": ["taxonomy_review_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-review\")\n\ntaxa_review_llm_chain = (\n taxonomy_review_prompt | taxonomy_generation_llm | StrOutputParser()\n).with_config(run_name=\"ReviewTaxonomy\")\n\n\nreview_taxonomy_chain = taxa_review_llm_chain | parse_taxa\n\n\ndef review_taxonomy(\n state: TaxonomyGenerationState, config: RunnableConfig\n) -> TaxonomyGenerationState:\n batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n original = state[\"documents\"]\n indices = list(range(len(original)))\n random.shuffle(indices)\n return invoke_taxonomy_chain(\n review_taxonomy_chain, state, config, indices[:batch_size]\n )"] }, { "cell_type": "markdown", @@ -470,40 +190,7 @@ "id": "f1f97ea4-53e5-4f55-8d73-b5b2234a47d9", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import StateGraph\n", - "\n", - "graph = StateGraph(TaxonomyGenerationState)\n", - "graph.add_node(\"summarize\", map_reduce_chain)\n", - "graph.add_node(\"get_minibatches\", get_minibatches)\n", - "graph.add_node(\"generate_taxonomy\", generate_taxonomy)\n", - "graph.add_node(\"update_taxonomy\", update_taxonomy)\n", - "graph.add_node(\"review_taxonomy\", review_taxonomy)\n", - "\n", - "graph.add_edge(\"summarize\", \"get_minibatches\")\n", - "graph.add_edge(\"get_minibatches\", \"generate_taxonomy\")\n", - "graph.add_edge(\"generate_taxonomy\", \"update_taxonomy\")\n", - "\n", - "\n", - "def should_review(state: TaxonomyGenerationState) -> str:\n", - " num_minibatches = len(state[\"minibatches\"])\n", - " num_revisions = len(state[\"clusters\"])\n", - " if num_revisions < num_minibatches:\n", - " return \"update_taxonomy\"\n", - " return \"review_taxonomy\"\n", - "\n", - "\n", - "graph.add_conditional_edges(\n", - " \"update_taxonomy\",\n", - " should_review,\n", - " # Optional (but required for the diagram to be drawn correctly below)\n", - " {\"update_taxonomy\": \"update_taxonomy\", \"review_taxonomy\": \"review_taxonomy\"},\n", - ")\n", - "graph.set_finish_point(\"review_taxonomy\")\n", - "\n", - "graph.set_entry_point(\"summarize\")\n", - "app = graph.compile()" - ] + "source": ["from langgraph.graph import StateGraph, START\n\ngraph = StateGraph(TaxonomyGenerationState)\ngraph.add_node(\"summarize\", map_reduce_chain)\ngraph.add_node(\"get_minibatches\", get_minibatches)\ngraph.add_node(\"generate_taxonomy\", generate_taxonomy)\ngraph.add_node(\"update_taxonomy\", update_taxonomy)\ngraph.add_node(\"review_taxonomy\", review_taxonomy)\n\ngraph.add_edge(\"summarize\", \"get_minibatches\")\ngraph.add_edge(\"get_minibatches\", \"generate_taxonomy\")\ngraph.add_edge(\"generate_taxonomy\", \"update_taxonomy\")\n\n\ndef should_review(state: TaxonomyGenerationState) -> str:\n num_minibatches = len(state[\"minibatches\"])\n num_revisions = len(state[\"clusters\"])\n if num_revisions < num_minibatches:\n return \"update_taxonomy\"\n return \"review_taxonomy\"\n\n\ngraph.add_conditional_edges(\n \"update_taxonomy\",\n should_review,\n # Optional (but required for the diagram to be drawn correctly below)\n {\"update_taxonomy\": \"update_taxonomy\", \"review_taxonomy\": \"review_taxonomy\"},\n)\ngraph.set_finish_point(\"review_taxonomy\")\n\ngraph.add_edge(START, \"summarize\")\napp = graph.compile()"] }, { "cell_type": "code", @@ -523,11 +210,7 @@ "output_type": "execute_result" } ], - "source": [ - "from IPython.display import Image\n", - "\n", - "Image(app.get_graph().draw_png())" - ] + "source": ["from IPython.display import Image\n\nImage(app.get_graph().draw_png())"] }, { "cell_type": "markdown", @@ -549,51 +232,7 @@ "id": "bcc65649-157f-4848-9ef0-8a9932a98d85", "metadata": {}, "outputs": [], - "source": [ - "from datetime import datetime, timedelta\n", - "\n", - "from langsmith import Client\n", - "\n", - "project_name = \"YOUR PROJECT NAME\" # Update to your own project\n", - "client = Client()\n", - "\n", - "past_week = datetime.now() - timedelta(days=7)\n", - "runs = list(\n", - " client.list_runs(\n", - " project_name=project_name,\n", - " filter=\"eq(is_root, true)\",\n", - " start_time=past_week,\n", - " # We only need to return the inputs + outputs\n", - " select=[\"inputs\", \"outputs\"],\n", - " )\n", - ")\n", - "\n", - "\n", - "# Convert the langsmith traces to our graph's Doc object.\n", - "def run_to_doc(run) -> Doc:\n", - " turns = []\n", - " idx = 0\n", - " for turn in run.inputs.get(\"chat_history\") or []:\n", - " key, value = next(iter(turn.items()))\n", - " turns.append(f\"<{key} idx={idx}>\\n{value}\\n\")\n", - " idx += 1\n", - " turns.append(\n", - " f\"\"\"\n", - "\n", - "{run.inputs['question']}\n", - "\"\"\"\n", - " )\n", - " if run.outputs and run.outputs[\"output\"]:\n", - " turns.append(\n", - " f\"\"\"\n", - "{run.outputs['output']}\n", - "\"\"\"\n", - " )\n", - " return {\n", - " \"id\": str(run.id),\n", - " \"content\": (\"\\n\".join(turns)),\n", - " }" - ] + "source": ["from datetime import datetime, timedelta\n\nfrom langsmith import Client\n\nproject_name = \"YOUR PROJECT NAME\" # Update to your own project\nclient = Client()\n\npast_week = datetime.now() - timedelta(days=7)\nruns = list(\n client.list_runs(\n project_name=project_name,\n filter=\"eq(is_root, true)\",\n start_time=past_week,\n # We only need to return the inputs + outputs\n select=[\"inputs\", \"outputs\"],\n )\n)\n\n\n# Convert the langsmith traces to our graph's Doc object.\ndef run_to_doc(run) -> Doc:\n turns = []\n idx = 0\n for turn in run.inputs.get(\"chat_history\") or []:\n key, value = next(iter(turn.items()))\n turns.append(f\"<{key} idx={idx}>\\n{value}\\n\")\n idx += 1\n turns.append(\n f\"\"\"\n\n{run.inputs['question']}\n\"\"\"\n )\n if run.outputs and run.outputs[\"output\"]:\n turns.append(\n f\"\"\"\n{run.outputs['output']}\n\"\"\"\n )\n return {\n \"id\": str(run.id),\n \"content\": (\"\\n\".join(turns)),\n }"] }, { "cell_type": "markdown", @@ -611,15 +250,7 @@ "id": "900906b5-9264-46a8-ba83-46307f8c25d0", "metadata": {}, "outputs": [], - "source": [ - "from langchain.cache import InMemoryCache\n", - "from langchain.globals import set_llm_cache\n", - "\n", - "# Optional. If you are running into errors or rate limits and want to avoid repeated computation,\n", - "# you can set this while debugging\n", - "\n", - "set_llm_cache(InMemoryCache())" - ] + "source": ["from langchain.cache import InMemoryCache\nfrom langchain.globals import set_llm_cache\n\n# Optional. If you are running into errors or rate limits and want to avoid repeated computation,\n# you can set this while debugging\n\nset_llm_cache(InMemoryCache())"] }, { "cell_type": "code", @@ -627,39 +258,7 @@ "id": "c2340177-f40c-407a-8e3e-cb06c2ef09ce", "metadata": {}, "outputs": [], - "source": [ - "# We will randomly sample down to 1K docs to speed things up\n", - "docs = [run_to_doc(run) for run in runs if run.inputs]\n", - "docs = random.sample(docs, min(len(docs), 1000))\n", - "use_case = (\n", - " \"Generate the taxonomy that can be used both to label the user intent\"\n", - " \" as well as to identify any required documentation (references, how-tos, etc.)\"\n", - " \" that would benefit the user.\"\n", - ")\n", - "\n", - "stream = app.stream(\n", - " {\"documents\": docs},\n", - " {\n", - " \"configurable\": {\n", - " \"use_case\": use_case,\n", - " # Optional:\n", - " \"batch_size\": 400,\n", - " \"suggestion_length\": 30,\n", - " \"cluster_name_length\": 10,\n", - " \"cluster_description_length\": 30,\n", - " \"explanation_length\": 20,\n", - " \"max_num_clusters\": 25,\n", - " },\n", - " # We batch summarize the docs. To avoid getting errors, we will limit the\n", - " # degree of parallelism to permit.\n", - " \"max_concurrency\": 2,\n", - " },\n", - ")\n", - "\n", - "for step in stream:\n", - " node, state = next(iter(step.items()))\n", - " print(node, str(state)[:20] + \" ...\")" - ] + "source": ["# We will randomly sample down to 1K docs to speed things up\ndocs = [run_to_doc(run) for run in runs if run.inputs]\ndocs = random.sample(docs, min(len(docs), 1000))\nuse_case = (\n \"Generate the taxonomy that can be used both to label the user intent\"\n \" as well as to identify any required documentation (references, how-tos, etc.)\"\n \" that would benefit the user.\"\n)\n\nstream = app.stream(\n {\"documents\": docs},\n {\n \"configurable\": {\n \"use_case\": use_case,\n # Optional:\n \"batch_size\": 400,\n \"suggestion_length\": 30,\n \"cluster_name_length\": 10,\n \"cluster_description_length\": 30,\n \"explanation_length\": 20,\n \"max_num_clusters\": 25,\n },\n # We batch summarize the docs. To avoid getting errors, we will limit the\n # degree of parallelism to permit.\n \"max_concurrency\": 2,\n },\n)\n\nfor step in stream:\n node, state = next(iter(step.items()))\n print(node, str(state)[:20] + \" ...\")"] }, { "cell_type": "markdown", @@ -719,31 +318,7 @@ "output_type": "execute_result" } ], - "source": [ - "from IPython.display import Markdown\n", - "\n", - "\n", - "def format_taxonomy_md(clusters):\n", - " md = \"## Final Taxonomy\\n\\n\"\n", - " md += \"| ID | Name | Description |\\n\"\n", - " md += \"|----|------|-------------|\\n\"\n", - "\n", - " # Fill the table with cluster data\n", - " for label in clusters:\n", - " id = label[\"id\"]\n", - " name = label[\"name\"].replace(\n", - " \"|\", \"\\\\|\"\n", - " ) # Escape any pipe characters within the content\n", - " description = label[\"description\"].replace(\n", - " \"|\", \"\\\\|\"\n", - " ) # Escape any pipe characters\n", - " md += f\"| {id} | {name} | {description} |\\n\"\n", - "\n", - " return md\n", - "\n", - "\n", - "Markdown(format_taxonomy_md(step[\"__end__\"][\"clusters\"][-1]))" - ] + "source": ["from IPython.display import Markdown\n\n\ndef format_taxonomy_md(clusters):\n md = \"## Final Taxonomy\\n\\n\"\n md += \"| ID | Name | Description |\\n\"\n md += \"|----|------|-------------|\\n\"\n\n # Fill the table with cluster data\n for label in clusters:\n id = label[\"id\"]\n name = label[\"name\"].replace(\n \"|\", \"\\\\|\"\n ) # Escape any pipe characters within the content\n description = label[\"description\"].replace(\n \"|\", \"\\\\|\"\n ) # Escape any pipe characters\n md += f\"| {id} | {name} | {description} |\\n\"\n\n return md\n\n\nMarkdown(format_taxonomy_md(step[\"__end__\"][\"clusters\"][-1]))"] }, { "cell_type": "markdown", @@ -773,32 +348,7 @@ "id": "8aa8a6f5-f53a-41e5-b09d-c6e8476e5471", "metadata": {}, "outputs": [], - "source": [ - "labeling_prompt = hub.pull(\"wfh/tnt-llm-classify\")\n", - "\n", - "labeling_llm = ChatAnthropic(model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000)\n", - "labeling_llm_chain = (labeling_prompt | labeling_llm | StrOutputParser()).with_config(\n", - " run_name=\"ClassifyDocs\"\n", - ")\n", - "\n", - "\n", - "def parse_labels(output_text: str) -> Dict:\n", - " \"\"\"Parse the generated labels from the predictions.\"\"\"\n", - " category_matches = re.findall(\n", - " r\"\\s*(.*?).*\",\n", - " output_text,\n", - " re.DOTALL,\n", - " )\n", - " categories = [{\"category\": category.strip()} for category in category_matches]\n", - " if len(categories) > 1:\n", - " logger.warning(f\"Multiple selected categories: {categories}\")\n", - " label = categories[0]\n", - " stripped = re.sub(r\"^\\d+\\.\\s*\", \"\", label[\"category\"]).strip()\n", - " return {\"category\": stripped}\n", - "\n", - "\n", - "labeling_chain = labeling_llm_chain | parse_labels" - ] + "source": ["labeling_prompt = hub.pull(\"wfh/tnt-llm-classify\")\n\nlabeling_llm = ChatAnthropic(model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000)\nlabeling_llm_chain = (labeling_prompt | labeling_llm | StrOutputParser()).with_config(\n run_name=\"ClassifyDocs\"\n)\n\n\ndef parse_labels(output_text: str) -> Dict:\n \"\"\"Parse the generated labels from the predictions.\"\"\"\n category_matches = re.findall(\n r\"\\s*(.*?).*\",\n output_text,\n re.DOTALL,\n )\n categories = [{\"category\": category.strip()} for category in category_matches]\n if len(categories) > 1:\n logger.warning(f\"Multiple selected categories: {categories}\")\n label = categories[0]\n stripped = re.sub(r\"^\\d+\\.\\s*\", \"\", label[\"category\"]).strip()\n return {\"category\": stripped}\n\n\nlabeling_chain = labeling_llm_chain | parse_labels"] }, { "cell_type": "code", @@ -806,23 +356,7 @@ "id": "59c06eea-ecbf-43af-a292-71816ccd92b8", "metadata": {}, "outputs": [], - "source": [ - "final_taxonomy = step[\"__end__\"][\"clusters\"][-1]\n", - "xml_taxonomy = format_taxonomy(final_taxonomy)\n", - "results = labeling_chain.batch(\n", - " [\n", - " {\n", - " \"content\": doc[\"content\"],\n", - " \"taxonomy\": xml_taxonomy,\n", - " }\n", - " for doc in docs\n", - " ],\n", - " {\"max_concurrency\": 5},\n", - " return_exceptions=True,\n", - ")\n", - "# Update the docs to include the categories\n", - "updated_docs = [{**doc, **category} for doc, category in zip(docs, results)]" - ] + "source": ["final_taxonomy = step[\"__end__\"][\"clusters\"][-1]\nxml_taxonomy = format_taxonomy(final_taxonomy)\nresults = labeling_chain.batch(\n [\n {\n \"content\": doc[\"content\"],\n \"taxonomy\": xml_taxonomy,\n }\n for doc in docs\n ],\n {\"max_concurrency\": 5},\n return_exceptions=True,\n)\n# Update the docs to include the categories\nupdated_docs = [{**doc, **category} for doc, category in zip(docs, results)]"] }, { "cell_type": "code", @@ -830,10 +364,7 @@ "id": "0ef9be82-278e-4501-8af9-70409ce15cc2", "metadata": {}, "outputs": [], - "source": [ - "if \"OPENAI_API_KEY\" not in os.environ:\n", - " os.environ[\"OPENAI_API_KEY\"] = getpass(\"Enter your OPENAI_API_KEY: \")" - ] + "source": ["if \"OPENAI_API_KEY\" not in os.environ:\n os.environ[\"OPENAI_API_KEY\"] = getpass(\"Enter your OPENAI_API_KEY: \")"] }, { "cell_type": "code", @@ -841,14 +372,7 @@ "id": "c21f787e-2dcb-49c2-9cc1-5284a1732fbc", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "# Consider using other embedding models here too!\n", - "encoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\n", - "vectors = encoder.embed_documents([doc[\"content\"] for doc in docs])\n", - "embedded_docs = [{**doc, \"embedding\": v} for doc, v in zip(updated_docs, vectors)]" - ] + "source": ["from langchain_openai import OpenAIEmbeddings\n\n# Consider using other embedding models here too!\nencoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\nvectors = encoder.embed_documents([doc[\"content\"] for doc in docs])\nembedded_docs = [{**doc, \"embedding\": v} for doc, v in zip(updated_docs, vectors)]"] }, { "cell_type": "markdown", @@ -877,51 +401,7 @@ ] } ], - "source": [ - "import numpy as np\n", - "from sklearn.linear_model import LogisticRegression\n", - "from sklearn.metrics import accuracy_score, f1_score\n", - "from sklearn.model_selection import train_test_split\n", - "from sklearn.utils import class_weight\n", - "\n", - "# Create a dictionary mapping category names to their indices in the taxonomy\n", - "category_to_index = {d[\"name\"]: i for i, d in enumerate(final_taxonomy)}\n", - "category_to_index[\"Other\"] = len(category_to_index)\n", - "# Convert category strings to numeric labels\n", - "labels = [\n", - " category_to_index.get(d[\"category\"], category_to_index[\"Other\"])\n", - " for d in embedded_docs\n", - "]\n", - "\n", - "label_vectors = [d[\"embedding\"] for d in embedded_docs]\n", - "\n", - "X_train, X_test, y_train, y_test = train_test_split(\n", - " label_vectors, labels, test_size=0.2, random_state=42\n", - ")\n", - "\n", - "# Calculate class weights\n", - "class_weights = class_weight.compute_class_weight(\n", - " class_weight=\"balanced\", classes=np.unique(y_train), y=y_train\n", - ")\n", - "class_weight_dict = dict(enumerate(class_weights))\n", - "\n", - "# Weight the classes to partially handle imbalanced data\n", - "model = LogisticRegression(class_weight=class_weight_dict)\n", - "model.fit(X_train, y_train)\n", - "\n", - "train_preds = model.predict(X_train)\n", - "test_preds = model.predict(X_test)\n", - "\n", - "train_acc = accuracy_score(y_train, train_preds)\n", - "test_acc = accuracy_score(y_test, test_preds)\n", - "train_f1 = f1_score(y_train, train_preds, average=\"weighted\")\n", - "test_f1 = f1_score(y_test, test_preds, average=\"weighted\")\n", - "\n", - "print(f\"Train Accuracy: {train_acc:.3f}\")\n", - "print(f\"Test Accuracy: {test_acc:.3f}\")\n", - "print(f\"Train F1 Score: {train_f1:.3f}\")\n", - "print(f\"Test F1 Score: {test_f1:.3f}\")" - ] + "source": ["import numpy as np\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import accuracy_score, f1_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils import class_weight\n\n# Create a dictionary mapping category names to their indices in the taxonomy\ncategory_to_index = {d[\"name\"]: i for i, d in enumerate(final_taxonomy)}\ncategory_to_index[\"Other\"] = len(category_to_index)\n# Convert category strings to numeric labels\nlabels = [\n category_to_index.get(d[\"category\"], category_to_index[\"Other\"])\n for d in embedded_docs\n]\n\nlabel_vectors = [d[\"embedding\"] for d in embedded_docs]\n\nX_train, X_test, y_train, y_test = train_test_split(\n label_vectors, labels, test_size=0.2, random_state=42\n)\n\n# Calculate class weights\nclass_weights = class_weight.compute_class_weight(\n class_weight=\"balanced\", classes=np.unique(y_train), y=y_train\n)\nclass_weight_dict = dict(enumerate(class_weights))\n\n# Weight the classes to partially handle imbalanced data\nmodel = LogisticRegression(class_weight=class_weight_dict)\nmodel.fit(X_train, y_train)\n\ntrain_preds = model.predict(X_train)\ntest_preds = model.predict(X_test)\n\ntrain_acc = accuracy_score(y_train, train_preds)\ntest_acc = accuracy_score(y_test, test_preds)\ntrain_f1 = f1_score(y_train, train_preds, average=\"weighted\")\ntest_f1 = f1_score(y_test, test_preds, average=\"weighted\")\n\nprint(f\"Train Accuracy: {train_acc:.3f}\")\nprint(f\"Test Accuracy: {test_acc:.3f}\")\nprint(f\"Train F1 Score: {train_f1:.3f}\")\nprint(f\"Test F1 Score: {test_f1:.3f}\")"] }, { "cell_type": "markdown", @@ -939,15 +419,7 @@ "id": "c27cbb6b-4d0f-476a-bef3-31ed307ce45f", "metadata": {}, "outputs": [], - "source": [ - "from joblib import dump as jl_dump\n", - "\n", - "categories = list(category_to_index)\n", - "\n", - "# Save the model and categories to a file\n", - "with open(\"model.joblib\", \"wb\") as file:\n", - " jl_dump((model, categories), file)" - ] + "source": ["from joblib import dump as jl_dump\n\ncategories = list(category_to_index)\n\n# Save the model and categories to a file\nwith open(\"model.joblib\", \"wb\") as file:\n jl_dump((model, categories), file)"] }, { "cell_type": "markdown", @@ -965,24 +437,7 @@ "id": "28f0b88a-b308-4208-b482-6c157357dfc6", "metadata": {}, "outputs": [], - "source": [ - "from joblib import load as jl_load\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "loaded_model, loaded_categories = jl_load(\"model.joblib\")\n", - "encoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\n", - "\n", - "\n", - "def get_category_name(predictions):\n", - " return [loaded_categories[pred] for pred in predictions]\n", - "\n", - "\n", - "classifier = (\n", - " RunnableLambda(encoder.embed_documents, encoder.aembed_documents)\n", - " | loaded_model.predict\n", - " | get_category_name\n", - ")" - ] + "source": ["from joblib import load as jl_load\nfrom langchain_openai import OpenAIEmbeddings\n\nloaded_model, loaded_categories = jl_load(\"model.joblib\")\nencoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\n\n\ndef get_category_name(predictions):\n return [loaded_categories[pred] for pred in predictions]\n\n\nclassifier = (\n RunnableLambda(encoder.embed_documents, encoder.aembed_documents)\n | loaded_model.predict\n | get_category_name\n)"] }, { "cell_type": "markdown", @@ -1000,22 +455,7 @@ "id": "6cdb9d8a-2aa1-4f48-8b23-f311fdf36416", "metadata": {}, "outputs": [], - "source": [ - "client = Client()\n", - "\n", - "past_5_min = datetime.now() - timedelta(minutes=5)\n", - "runs = list(\n", - " client.list_runs(\n", - " project_name=project_name,\n", - " filter=\"eq(is_root, true)\",\n", - " start_time=past_5_min,\n", - " # We only need to return the inputs + outputs\n", - " select=[\"inputs\", \"outputs\"],\n", - " limit=100,\n", - " )\n", - ")\n", - "docs = [run_to_doc(r) for r in runs]" - ] + "source": ["client = Client()\n\npast_5_min = datetime.now() - timedelta(minutes=5)\nruns = list(\n client.list_runs(\n project_name=project_name,\n filter=\"eq(is_root, true)\",\n start_time=past_5_min,\n # We only need to return the inputs + outputs\n select=[\"inputs\", \"outputs\"],\n limit=100,\n )\n)\ndocs = [run_to_doc(r) for r in runs]"] }, { "cell_type": "code", @@ -1038,10 +478,7 @@ ] } ], - "source": [ - "classes = classifier.invoke([doc[\"content\"] for doc in docs])\n", - "print(classes[:2])" - ] + "source": ["classes = classifier.invoke([doc[\"content\"] for doc in docs])\nprint(classes[:2])"] }, { "cell_type": "markdown", diff --git a/examples/usaco/usaco.ipynb b/examples/usaco/usaco.ipynb index 7abeb566c..c83c84df6 100644 --- a/examples/usaco/usaco.ipynb +++ b/examples/usaco/usaco.ipynb @@ -43,10 +43,7 @@ "id": "c686827a-8078-4fd4-af7a-638ca1362796", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langsmith langchain_anthropic datasets langchain langchainhub" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph langsmith langchain_anthropic datasets langchain langchainhub"] }, { "cell_type": "code", @@ -54,21 +51,7 @@ "id": "e2e542bb-a99e-44d3-8ebb-6a952dcbf2bf", "metadata": {}, "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _get_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_get_env(\"ANTHROPIC_API_KEY\")\n", - "# Recommended\n", - "_get_env(\"LANGCHAIN_API_KEY\")\n", - "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"" - ] + "source": ["import getpass\nimport os\n\n\ndef _get_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_get_env(\"ANTHROPIC_API_KEY\")\n# Recommended\n_get_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""] }, { "cell_type": "markdown", @@ -86,28 +69,7 @@ "id": "f7a0c7bd-512d-4e5b-ab43-1bc3b8c97fd4", "metadata": {}, "outputs": [], - "source": [ - "import os\n", - "import zipfile\n", - "\n", - "import datasets\n", - "import requests\n", - "\n", - "usaco_url = \"https://storage.googleapis.com/benchmarks-artifacts/usaco/usaco_sampled_with_tests.zip\"\n", - "zip_path = \"usaco.zip\"\n", - "extract_path = \"usaco_datasets\"\n", - "\n", - "response = requests.get(usaco_url)\n", - "with open(zip_path, \"wb\") as file:\n", - " file.write(response.content)\n", - "\n", - "with zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n", - " zip_ref.extractall(extract_path)\n", - "\n", - "os.remove(zip_path)\n", - "\n", - "ds = datasets.load_from_disk(os.path.join(extract_path, \"usaco_v3_sampled_with_tests\"))" - ] + "source": ["import os\nimport zipfile\n\nimport datasets\nimport requests\n\nusaco_url = \"https://storage.googleapis.com/benchmarks-artifacts/usaco/usaco_sampled_with_tests.zip\"\nzip_path = \"usaco.zip\"\nextract_path = \"usaco_datasets\"\n\nresponse = requests.get(usaco_url)\nwith open(zip_path, \"wb\") as file:\n file.write(response.content)\n\nwith zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n zip_ref.extractall(extract_path)\n\nos.remove(zip_path)\n\nds = datasets.load_from_disk(os.path.join(extract_path, \"usaco_v3_sampled_with_tests\"))"] }, { "cell_type": "markdown", @@ -126,72 +88,7 @@ "id": "54f9d037-121e-412f-857a-3e0ccc73892e", "metadata": {}, "outputs": [], - "source": [ - "import multiprocessing\n", - "import queue\n", - "import subprocess\n", - "import sys\n", - "import time\n", - "import traceback\n", - "\n", - "multiprocessing.set_start_method(\"fork\", force=True)\n", - "# WARNING\n", - "# This program exists to execute untrusted model-generated code. Although\n", - "# it is highly unlikely that model-generated code will do something overtly\n", - "# malicious in response to this test suite, model-generated code may act\n", - "# destructively due to a lack of model capability or alignment.\n", - "# Users are strongly encouraged to sandbox this evaluation suite so that it\n", - "# does not perform destructive actions on their host or network.\n", - "# Proceed at your own risk:\n", - "\n", - "\n", - "def exec_program(q, program, input_data, expected_output, timeout):\n", - " try:\n", - " start_time = time.time()\n", - " process = subprocess.Popen(\n", - " [sys.executable, \"-c\", program],\n", - " stdin=subprocess.PIPE,\n", - " stdout=subprocess.PIPE,\n", - " stderr=subprocess.PIPE,\n", - " text=True,\n", - " )\n", - " stdout, stderr = process.communicate(input=input_data, timeout=timeout)\n", - " if time.time() - start_time > timeout:\n", - " raise TimeoutError(\"Execution timed out.\")\n", - " if process.returncode != 0:\n", - " q.put(f\"failed: {stderr}\")\n", - " else:\n", - " if stdout.strip() == expected_output.strip():\n", - " q.put(\"passed\")\n", - " else:\n", - " q.put(f\"wrong answer. Expected '{expected_output}', got '{stdout}'\")\n", - " except subprocess.TimeoutExpired:\n", - " process.kill()\n", - " q.put(\"timed out\")\n", - " except Exception:\n", - " q.put(f\"failed: {traceback.format_exc()}\")\n", - "\n", - "\n", - "def check_correctness(\n", - " program: str, input_data: str, expected_output: str, timeout: float\n", - ") -> str:\n", - " q = multiprocessing.Queue()\n", - " process = multiprocessing.Process(\n", - " target=exec_program, args=(q, program, input_data, expected_output, timeout)\n", - " )\n", - " process.start()\n", - " process.join(timeout=timeout + 1)\n", - " if process.is_alive():\n", - " process.terminate()\n", - " process.join()\n", - " result = \"timed out\"\n", - " else:\n", - " try:\n", - " result = q.get_nowait()\n", - " except queue.Empty:\n", - " result = \"no result returned\"\n", - " return result" - ] + "source": ["import multiprocessing\nimport queue\nimport subprocess\nimport sys\nimport time\nimport traceback\n\nmultiprocessing.set_start_method(\"fork\", force=True)\n# WARNING\n# This program exists to execute untrusted model-generated code. Although\n# it is highly unlikely that model-generated code will do something overtly\n# malicious in response to this test suite, model-generated code may act\n# destructively due to a lack of model capability or alignment.\n# Users are strongly encouraged to sandbox this evaluation suite so that it\n# does not perform destructive actions on their host or network.\n# Proceed at your own risk:\n\n\ndef exec_program(q, program, input_data, expected_output, timeout):\n try:\n start_time = time.time()\n process = subprocess.Popen(\n [sys.executable, \"-c\", program],\n stdin=subprocess.PIPE,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n )\n stdout, stderr = process.communicate(input=input_data, timeout=timeout)\n if time.time() - start_time > timeout:\n raise TimeoutError(\"Execution timed out.\")\n if process.returncode != 0:\n q.put(f\"failed: {stderr}\")\n else:\n if stdout.strip() == expected_output.strip():\n q.put(\"passed\")\n else:\n q.put(f\"wrong answer. Expected '{expected_output}', got '{stdout}'\")\n except subprocess.TimeoutExpired:\n process.kill()\n q.put(\"timed out\")\n except Exception:\n q.put(f\"failed: {traceback.format_exc()}\")\n\n\ndef check_correctness(\n program: str, input_data: str, expected_output: str, timeout: float\n) -> str:\n q = multiprocessing.Queue()\n process = multiprocessing.Process(\n target=exec_program, args=(q, program, input_data, expected_output, timeout)\n )\n process.start()\n process.join(timeout=timeout + 1)\n if process.is_alive():\n process.terminate()\n process.join()\n result = \"timed out\"\n else:\n try:\n result = q.get_nowait()\n except queue.Empty:\n result = \"no result returned\"\n return result"] }, { "cell_type": "markdown", @@ -217,17 +114,7 @@ ] } ], - "source": [ - "program_code = \"print('hello, world!')\"\n", - "input_data = \"\"\n", - "expected_output = \"hello, world!\"\n", - "timeout = 2\n", - "\n", - "test_result = check_correctness(program_code, input_data, expected_output, timeout)\n", - "print(\"Example 1: \", test_result)\n", - "test_result = check_correctness(\"print('goodbye')\", input_data, \"hi there\", timeout)\n", - "print(\"Example 2: \", test_result)" - ] + "source": ["program_code = \"print('hello, world!')\"\ninput_data = \"\"\nexpected_output = \"hello, world!\"\ntimeout = 2\n\ntest_result = check_correctness(program_code, input_data, expected_output, timeout)\nprint(\"Example 1: \", test_result)\ntest_result = check_correctness(\"print('goodbye')\", input_data, \"hi there\", timeout)\nprint(\"Example 2: \", test_result)"] }, { "cell_type": "markdown", @@ -265,27 +152,7 @@ "id": "f43d68d9-10be-4544-879a-88a33db18bea", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "class TestCase(TypedDict):\n", - " inputs: str\n", - " outputs: str\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # Append-only chat memory so the agent can try to recover from initial mistakes.\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " # From the dataset. These are used for testing.\n", - " test_cases: list[TestCase]\n", - " runtime_limit: int\n", - " status: str" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass TestCase(TypedDict):\n inputs: str\n outputs: str\n\n\nclass State(TypedDict):\n # Append-only chat memory so the agent can try to recover from initial mistakes.\n messages: Annotated[list[AnyMessage], add_messages]\n # From the dataset. These are used for testing.\n test_cases: list[TestCase]\n runtime_limit: int\n status: str"] }, { "cell_type": "markdown", @@ -301,18 +168,7 @@ "id": "6d56776f-993b-4ca7-89ef-21dec01dc9d3", "metadata": {}, "outputs": [], - "source": [ - "input_states = [\n", - " {\n", - " \"messages\": [(\"user\", row[\"description\"])],\n", - " \"test_cases\": row[\"test_cases\"],\n", - " \"runtime_limit\": row[\"runtime_limit\"],\n", - " \"status\": \"in_progress\",\n", - " \"problem_level\": row[\"problem_level\"],\n", - " }\n", - " for row in ds\n", - "]" - ] + "source": ["input_states = [\n {\n \"messages\": [(\"user\", row[\"description\"])],\n \"test_cases\": row[\"test_cases\"],\n \"runtime_limit\": row[\"runtime_limit\"],\n \"status\": \"in_progress\",\n \"problem_level\": row[\"problem_level\"],\n }\n for row in ds\n]"] }, { "cell_type": "markdown", @@ -330,28 +186,7 @@ "id": "7b9e7742-16a3-4ad2-bc63-5f9cd4fd734b", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.language_models import BaseChatModel\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class writePython(BaseModel):\n", - " \"\"\"Write python code that resolves the problem.\"\"\"\n", - "\n", - " reasoning: str = Field(..., description=\"Conceptual solution.\")\n", - " pseudocode: str = Field(..., description=\"Detailed English pseudocode.\")\n", - " code: str = Field(..., description=\"Valid Python 3 solution to the problem\")\n", - "\n", - "\n", - "class Solver:\n", - " def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n", - " self.runnable = prompt | llm.bind_tools([writePython])\n", - "\n", - " def __call__(self, state: State) -> dict:\n", - " # Our agent only can see the \"messages\" and will ignore the test info\n", - " return {\"messages\": [self.runnable.invoke({\"messages\": state[\"messages\"]})]}" - ] + "source": ["from langchain_core.language_models import BaseChatModel\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass writePython(BaseModel):\n \"\"\"Write python code that resolves the problem.\"\"\"\n\n reasoning: str = Field(..., description=\"Conceptual solution.\")\n pseudocode: str = Field(..., description=\"Detailed English pseudocode.\")\n code: str = Field(..., description=\"Valid Python 3 solution to the problem\")\n\n\nclass Solver:\n def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n self.runnable = prompt | llm.bind_tools([writePython])\n\n def __call__(self, state: State) -> dict:\n # Our agent only can see the \"messages\" and will ignore the test info\n return {\"messages\": [self.runnable.invoke({\"messages\": state[\"messages\"]})]}"] }, { "cell_type": "markdown", @@ -396,22 +231,7 @@ ] } ], - "source": [ - "from langchain import hub\n", - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "# For this section, we are testing zero-shot performance and won't have\n", - "# any examples. Partial them out to pre-fill the template.\n", - "prompt = hub.pull(\"wfh/usaco-draft-solver\").partial(examples=\"\")\n", - "print(\"*\" * 35 + \"Prompt\" + \"*\" * 35)\n", - "prompt.pretty_print()\n", - "\n", - "# Use Haiku if you want to save $$ while (almost) never correctly answering the question\n", - "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n", - "llm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", - "\n", - "solver = Solver(llm, prompt)" - ] + "source": ["from langchain import hub\nfrom langchain_anthropic import ChatAnthropic\n\n# For this section, we are testing zero-shot performance and won't have\n# any examples. Partial them out to pre-fill the template.\nprompt = hub.pull(\"wfh/usaco-draft-solver\").partial(examples=\"\")\nprint(\"*\" * 35 + \"Prompt\" + \"*\" * 35)\nprompt.pretty_print()\n\n# Use Haiku if you want to save $$ while (almost) never correctly answering the question\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n\nsolver = Solver(llm, prompt)"] }, { "cell_type": "code", @@ -430,25 +250,7 @@ ] } ], - "source": [ - "print(\"*\" * 34 + \" Example \" + \"*\" * 34)\n", - "result = solver(\n", - " {\n", - " \"messages\": [\n", - " (\n", - " \"user\",\n", - " \"How do I get a perfectly random sample from an infinite stream\",\n", - " )\n", - " ]\n", - " }\n", - ")\n", - "result[\"messages\"][0].pretty_print()\n", - "# Could expand to include (1)\n", - "# 1. Restate the problem in plain English\n", - "# 2. Closely following the explanation, restate and explain the solution in plain English\n", - "# 3. Write a pseudocode solution\n", - "# 4. Output the final Python solution with your solution steps in comments." - ] + "source": ["print(\"*\" * 34 + \" Example \" + \"*\" * 34)\nresult = solver(\n {\n \"messages\": [\n (\n \"user\",\n \"How do I get a perfectly random sample from an infinite stream\",\n )\n ]\n }\n)\nresult[\"messages\"][0].pretty_print()\n# Could expand to include (1)\n# 1. Restate the problem in plain English\n# 2. Closely following the explanation, restate and explain the solution in plain English\n# 3. Write a pseudocode solution\n# 4. Output the final Python solution with your solution steps in comments."] }, { "cell_type": "markdown", @@ -467,57 +269,7 @@ "id": "1785015b-24f8-415f-b950-e229b5137887", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", - "\n", - "\n", - "# This is the node we will add to the graph.\n", - "# Most tool-calling APIs require that the `ToolMessage` contain the ID\n", - "# of the\n", - "def format_tool_message(response: str, ai_message: AIMessage):\n", - " return ToolMessage(\n", - " content=response + \"\\nMake all fixes using the writePython tool.\",\n", - " tool_call_id=ai_message.tool_calls[0][\"id\"],\n", - " )\n", - "\n", - "\n", - "def evaluate(state: State):\n", - " test_cases = state[\"test_cases\"]\n", - " ai_message: AIMessage = state[\"messages\"][-1]\n", - " if not ai_message.tool_calls:\n", - " return {\n", - " \"messages\": [\n", - " HumanMessage(\n", - " content=\"No code submitted. Please try again using the correct python code.\"\n", - " )\n", - " ]\n", - " }\n", - " try:\n", - " code = ai_message.tool_calls[0][\"args\"][\"code\"]\n", - " except Exception as e:\n", - " return {\"messages\": [format_tool_message(repr(e), ai_message)]}\n", - " num_test_cases = len(test_cases)\n", - " succeeded = 0\n", - " test_results = []\n", - " # TODO: Multiprocess\n", - " for test_case in test_cases:\n", - " input_data = test_case[\"inputs\"]\n", - " expected_output = test_case[\"outputs\"]\n", - " test_result = check_correctness(code, input_data, expected_output, timeout)\n", - " test_results.append(test_result)\n", - " if test_result == \"passed\":\n", - " succeeded += 1\n", - " pass_rate = succeeded / num_test_cases if num_test_cases else \"N/A\"\n", - " if pass_rate == 1:\n", - " return {\"status\": \"success\"}\n", - "\n", - " responses = \"\\n\".join(\n", - " [f\"\\n{r}\\n\" for i, r in enumerate(test_results)]\n", - " )\n", - " response = f\"Incorrect submission. Please respond with updated code.\\nPass rate: {succeeded}/{num_test_cases}\\nResults:\\n{responses}\"\n", - " formatted_message = format_tool_message(response, ai_message)\n", - " return {\"messages\": [formatted_message]}" - ] + "source": ["from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n\n\n# This is the node we will add to the graph.\n# Most tool-calling APIs require that the `ToolMessage` contain the ID\n# of the\ndef format_tool_message(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response + \"\\nMake all fixes using the writePython tool.\",\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef evaluate(state: State):\n test_cases = state[\"test_cases\"]\n ai_message: AIMessage = state[\"messages\"][-1]\n if not ai_message.tool_calls:\n return {\n \"messages\": [\n HumanMessage(\n content=\"No code submitted. Please try again using the correct python code.\"\n )\n ]\n }\n try:\n code = ai_message.tool_calls[0][\"args\"][\"code\"]\n except Exception as e:\n return {\"messages\": [format_tool_message(repr(e), ai_message)]}\n num_test_cases = len(test_cases)\n succeeded = 0\n test_results = []\n # TODO: Multiprocess\n for test_case in test_cases:\n input_data = test_case[\"inputs\"]\n expected_output = test_case[\"outputs\"]\n test_result = check_correctness(code, input_data, expected_output, timeout)\n test_results.append(test_result)\n if test_result == \"passed\":\n succeeded += 1\n pass_rate = succeeded / num_test_cases if num_test_cases else \"N/A\"\n if pass_rate == 1:\n return {\"status\": \"success\"}\n\n responses = \"\\n\".join(\n [f\"\\n{r}\\n\" for i, r in enumerate(test_results)]\n )\n response = f\"Incorrect submission. Please respond with updated code.\\nPass rate: {succeeded}/{num_test_cases}\\nResults:\\n{responses}\"\n formatted_message = format_tool_message(response, ai_message)\n return {\"messages\": [formatted_message]}"] }, { "cell_type": "markdown", @@ -543,25 +295,7 @@ "id": "caf1560e-1517-4229-8a43-186816da6a3a", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.graph import END, StateGraph\n", - "\n", - "builder = StateGraph(State)\n", - "builder.add_node(\"solver\", solver)\n", - "builder.set_entry_point(\"solver\")\n", - "builder.add_node(\"evaluate\", evaluate)\n", - "builder.add_edge(\"solver\", \"evaluate\")\n", - "\n", - "\n", - "def control_edge(state: State):\n", - " if state.get(\"status\") == \"success\":\n", - " return END\n", - " return \"solver\"\n", - "\n", - "\n", - "builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solver\": \"solver\"})\n", - "graph = builder.compile()" - ] + "source": ["from langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"solver\", solver)\nbuilder.add_edge(START, \"solver\")\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"solver\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solver\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solver\": \"solver\"})\ngraph = builder.compile()"] }, { "cell_type": "code", @@ -580,15 +314,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -667,12 +393,7 @@ ] } ], - "source": [ - "input_state = input_states[0].copy()\n", - "# We will reduce the test cases to speed this notebook up\n", - "input_state[\"test_cases\"] = input_state[\"test_cases\"][:3]\n", - "print(input_state[\"messages\"][0][1])" - ] + "source": ["input_state = input_states[0].copy()\n# We will reduce the test cases to speed this notebook up\ninput_state[\"test_cases\"] = input_state[\"test_cases\"][:3]\nprint(input_state[\"messages\"][0][1])"] }, { "cell_type": "markdown", @@ -734,33 +455,7 @@ ] } ], - "source": [ - "from langchain_core.tracers.context import tracing_v2_enabled\n", - "from langsmith import Client\n", - "\n", - "\n", - "# We don't need to include all the test cases in our traces.\n", - "def _hide_test_cases(inputs):\n", - " copied = inputs.copy()\n", - " # These are tens of MB in size. No need to send them up\n", - " copied[\"test_cases\"] = \"...\"\n", - " return copied\n", - "\n", - "\n", - "client = Client(hide_inputs=_hide_test_cases, hide_outputs=_hide_test_cases)\n", - "with tracing_v2_enabled(client=client):\n", - " events = graph.stream(input_state)\n", - " for event in events:\n", - " for value in event.values():\n", - " messages = value.get(\"messages\")\n", - " if messages:\n", - " if isinstance(messages, list):\n", - " messages = value[\"messages\"][-1]\n", - " print(\n", - " \"Assistant:\",\n", - " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", - " )" - ] + "source": ["from langchain_core.tracers.context import tracing_v2_enabled\nfrom langsmith import Client\n\n\n# We don't need to include all the test cases in our traces.\ndef _hide_test_cases(inputs):\n copied = inputs.copy()\n # These are tens of MB in size. No need to send them up\n copied[\"test_cases\"] = \"...\"\n return copied\n\n\nclient = Client(hide_inputs=_hide_test_cases, hide_outputs=_hide_test_cases)\nwith tracing_v2_enabled(client=client):\n events = graph.stream(input_state)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )"] }, { "cell_type": "markdown", @@ -806,10 +501,7 @@ "id": "d612dd8d-31af-426c-944b-203acd55ace0", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --upgrade --quiet rank_bm25" - ] + "source": ["%%capture --no-stderr\n%pip install --upgrade --quiet rank_bm25"] }, { "cell_type": "markdown", @@ -827,29 +519,7 @@ "id": "16937fef-58b9-4ab2-bbfc-5237aad235ec", "metadata": {}, "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph.message import AnyMessage, add_messages\n", - "\n", - "\n", - "class TestCase(TypedDict):\n", - " inputs: str\n", - " outputs: str\n", - "\n", - "\n", - "class State(TypedDict):\n", - " # NEW! Candidate for retrieval + formatted fetched examples as \"memory\"\n", - " candidate: AIMessage\n", - " examples: str\n", - " # Repeated from Part 1\n", - " messages: Annotated[list[AnyMessage], add_messages]\n", - " test_cases: list[TestCase]\n", - " runtime_limit: int\n", - " status: str" - ] + "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass TestCase(TypedDict):\n inputs: str\n outputs: str\n\n\nclass State(TypedDict):\n # NEW! Candidate for retrieval + formatted fetched examples as \"memory\"\n candidate: AIMessage\n examples: str\n # Repeated from Part 1\n messages: Annotated[list[AnyMessage], add_messages]\n test_cases: list[TestCase]\n runtime_limit: int\n status: str"] }, { "cell_type": "markdown", @@ -867,40 +537,7 @@ "id": "25f947a7-15bb-4119-a47e-b5c33ca0a249", "metadata": {}, "outputs": [], - "source": [ - "from langchain import hub\n", - "from langchain_anthropic import ChatAnthropic\n", - "\n", - "\n", - "class Solver:\n", - " def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n", - " self.runnable = prompt | llm.bind_tools([writePython])\n", - "\n", - " def __call__(self, state: State) -> dict:\n", - " # Our agent only can see the \"messages\" and will ignore the test info\n", - " inputs = {\"messages\": state[\"messages\"]}\n", - " has_examples = bool(state.get(\"examples\"))\n", - " output_key = \"candidate\" # Used in the draft node\n", - " if has_examples:\n", - " output_key = \"messages\"\n", - " # Used in the solve node\n", - " inputs[\"examples\"] = state[\"examples\"]\n", - " response = self.runnable.invoke(inputs)\n", - " if not response.content:\n", - " return {\n", - " output_key: AIMessage(\n", - " content=\"I'll need to think about this step by step.\"\n", - " )\n", - " }\n", - " return {output_key: response}\n", - "\n", - "\n", - "prompt = hub.pull(\"wfh/usaco-draft-solver\")\n", - "llm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", - "\n", - "draft_solver = Solver(llm, prompt.partial(examples=\"\"))\n", - "solver = Solver(llm, prompt)" - ] + "source": ["from langchain import hub\nfrom langchain_anthropic import ChatAnthropic\n\n\nclass Solver:\n def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n self.runnable = prompt | llm.bind_tools([writePython])\n\n def __call__(self, state: State) -> dict:\n # Our agent only can see the \"messages\" and will ignore the test info\n inputs = {\"messages\": state[\"messages\"]}\n has_examples = bool(state.get(\"examples\"))\n output_key = \"candidate\" # Used in the draft node\n if has_examples:\n output_key = \"messages\"\n # Used in the solve node\n inputs[\"examples\"] = state[\"examples\"]\n response = self.runnable.invoke(inputs)\n if not response.content:\n return {\n output_key: AIMessage(\n content=\"I'll need to think about this step by step.\"\n )\n }\n return {output_key: response}\n\n\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nsolver = Solver(llm, prompt)"] }, { "cell_type": "markdown", @@ -918,13 +555,7 @@ "id": "e5e0aa40-79a4-4071-9ad2-9aa2f36599ce", "metadata": {}, "outputs": [], - "source": [ - "# We will test our agent on index 0 (the same as above).\n", - "# Later, we will test on index 2 (the first 'silver difficulty' question)\n", - "test_indices = [0, 2]\n", - "train_ds = [row for i, row in enumerate(ds) if i not in test_indices]\n", - "test_ds = [row for i, row in enumerate(ds) if i in test_indices]" - ] + "source": ["# We will test our agent on index 0 (the same as above).\n# Later, we will test on index 2 (the first 'silver difficulty' question)\ntest_indices = [0, 2]\ntrain_ds = [row for i, row in enumerate(ds) if i not in test_indices]\ntest_ds = [row for i, row in enumerate(ds) if i in test_indices]"] }, { "cell_type": "code", @@ -932,25 +563,7 @@ "id": "96a1ff96-7556-4959-9f54-1ade3bd1c01a", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.retrievers import BM25Retriever\n", - "\n", - "\n", - "def format_example(row):\n", - " question = row[\"description\"]\n", - " answer = row[\"solution\"]\n", - " return f\"\"\"\n", - "{question}\n", - "\n", - "\n", - "{answer}\n", - "\"\"\"\n", - "\n", - "\n", - "# Skip our 'test examples' to avoid cheating\n", - "# This is \"simulating\" having seen other in-context examples\n", - "retriever = BM25Retriever.from_texts([format_example(row) for row in train_ds])" - ] + "source": ["from langchain_community.retrievers import BM25Retriever\n\n\ndef format_example(row):\n question = row[\"description\"]\n answer = row[\"solution\"]\n return f\"\"\"\n{question}\n\n\n{answer}\n\"\"\"\n\n\n# Skip our 'test examples' to avoid cheating\n# This is \"simulating\" having seen other in-context examples\nretriever = BM25Retriever.from_texts([format_example(row) for row in train_ds])"] }, { "cell_type": "markdown", @@ -967,28 +580,7 @@ "id": "af42962d-c06e-4b6e-96df-72ad48f17617", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.runnables import RunnableConfig\n", - "\n", - "\n", - "def retrieve_examples(state: State, config: RunnableConfig):\n", - " top_k = config[\"configurable\"].get(\"k\") or 2\n", - " ai_message: AIMessage = state[\"candidate\"]\n", - " if not ai_message.tool_calls:\n", - " # We err here. To make more robust, you could loop back\n", - " raise ValueError(\"Draft agent did not produce a valid code block\")\n", - " code = ai_message.tool_calls[0][\"args\"][\"code\"]\n", - " examples_str = \"\\n\".join(\n", - " [doc.page_content for doc in retriever.invoke(code)[:top_k]]\n", - " )\n", - " examples_str = f\"\"\"\n", - "You previously solved the following problems in this competition:\n", - "\n", - "{examples_str}\n", - "\n", - "Approach this new question with similar sophistication.\"\"\"\n", - " return {\"examples\": examples_str}" - ] + "source": ["from langchain_core.runnables import RunnableConfig\n\n\ndef retrieve_examples(state: State, config: RunnableConfig):\n top_k = config[\"configurable\"].get(\"k\") or 2\n ai_message: AIMessage = state[\"candidate\"]\n if not ai_message.tool_calls:\n # We err here. To make more robust, you could loop back\n raise ValueError(\"Draft agent did not produce a valid code block\")\n code = ai_message.tool_calls[0][\"args\"][\"code\"]\n examples_str = \"\\n\".join(\n [doc.page_content for doc in retriever.invoke(code)[:top_k]]\n )\n examples_str = f\"\"\"\nYou previously solved the following problems in this competition:\n\n{examples_str}\n\nApproach this new question with similar sophistication.\"\"\"\n return {\"examples\": examples_str}"] }, { "cell_type": "markdown", @@ -1006,34 +598,7 @@ "id": "e6e73e85-1232-4848-beba-3139ac7d0a64", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "builder = StateGraph(State)\n", - "builder.add_node(\"draft\", draft_solver)\n", - "builder.set_entry_point(\"draft\")\n", - "builder.add_node(\"retrieve\", retrieve_examples)\n", - "builder.add_node(\"solve\", solver)\n", - "builder.add_node(\"evaluate\", evaluate)\n", - "# Add connectivity\n", - "builder.add_edge(\"draft\", \"retrieve\")\n", - "builder.add_edge(\"retrieve\", \"solve\")\n", - "builder.add_edge(\"solve\", \"evaluate\")\n", - "\n", - "\n", - "def control_edge(state: State):\n", - " if state.get(\"status\") == \"success\":\n", - " return END\n", - " return \"solve\"\n", - "\n", - "\n", - "builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n", - "\n", - "\n", - "checkpointer = SqliteSaver.from_conn_string(\":memory:\")\n", - "graph = builder.compile(checkpointer=checkpointer)" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=checkpointer)"] }, { "cell_type": "code", @@ -1052,15 +617,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -1093,25 +650,7 @@ ] } ], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"question-recall\", \"k\": 3}}\n", - "with tracing_v2_enabled(client=client):\n", - " events = graph.stream(input_state, config)\n", - " for event in events:\n", - " for value in event.values():\n", - " messages = value.get(\"messages\")\n", - " if messages:\n", - " if isinstance(messages, list):\n", - " messages = value[\"messages\"][-1]\n", - " print(\n", - " \"Assistant:\",\n", - " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", - " )\n", - " elif value.get(\"examples\"):\n", - " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", - " elif value.get(\"candidate\"):\n", - " print(str(value[\"candidate\"].content)[:200])" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"question-recall\", \"k\": 3}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(input_state, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] }, { "cell_type": "markdown", @@ -1138,10 +677,7 @@ "output_type": "execute_result" } ], - "source": [ - "checkpoint = graph.get_state(config)\n", - "checkpoint.values[\"status\"]" - ] + "source": ["checkpoint = graph.get_state(config)\ncheckpoint.values[\"status\"]"] }, { "cell_type": "markdown", @@ -1170,10 +706,7 @@ "output_type": "execute_result" } ], - "source": [ - "silver_row = test_ds[1]\n", - "silver_row[\"problem_level\"]" - ] + "source": ["silver_row = test_ds[1]\nsilver_row[\"problem_level\"]"] }, { "cell_type": "code", @@ -1231,33 +764,7 @@ ] } ], - "source": [ - "silver_input = {\n", - " \"messages\": [(\"user\", silver_row[\"description\"])],\n", - " \"test_cases\": silver_row[\"test_cases\"],\n", - " \"runtime_limit\": silver_row[\"runtime_limit\"],\n", - " \"status\": \"in_progress\",\n", - "}\n", - "\n", - "\n", - "config = {\"configurable\": {\"thread_id\": \"silver-question-1\", \"k\": 2}}\n", - "with tracing_v2_enabled(client=client):\n", - " events = graph.stream(silver_input, config)\n", - " for event in events:\n", - " for value in event.values():\n", - " messages = value.get(\"messages\")\n", - " if messages:\n", - " if isinstance(messages, list):\n", - " messages = value[\"messages\"][-1]\n", - " print(\n", - " \"Assistant:\",\n", - " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", - " )\n", - " elif value.get(\"examples\"):\n", - " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", - " elif value.get(\"candidate\"):\n", - " print(str(value[\"candidate\"].content)[:200])" - ] + "source": ["silver_input = {\n \"messages\": [(\"user\", silver_row[\"description\"])],\n \"test_cases\": silver_row[\"test_cases\"],\n \"runtime_limit\": silver_row[\"runtime_limit\"],\n \"status\": \"in_progress\",\n}\n\n\nconfig = {\"configurable\": {\"thread_id\": \"silver-question-1\", \"k\": 2}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(silver_input, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] }, { "cell_type": "markdown", @@ -1302,36 +809,7 @@ "id": "3c6456ba-363c-4133-8631-6dabb042b6ce", "metadata": {}, "outputs": [], - "source": [ - "# This is all the same as before\n", - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "from langgraph.graph import END, StateGraph\n", - "\n", - "builder = StateGraph(State)\n", - "prompt = hub.pull(\"wfh/usaco-draft-solver\")\n", - "llm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n", - "\n", - "draft_solver = Solver(llm, prompt.partial(examples=\"\"))\n", - "builder.add_node(\"draft\", draft_solver)\n", - "builder.set_entry_point(\"draft\")\n", - "builder.add_node(\"retrieve\", retrieve_examples)\n", - "solver = Solver(llm, prompt)\n", - "builder.add_node(\"solve\", solver)\n", - "builder.add_node(\"evaluate\", evaluate)\n", - "builder.add_edge(\"draft\", \"retrieve\")\n", - "builder.add_edge(\"retrieve\", \"solve\")\n", - "builder.add_edge(\"solve\", \"evaluate\")\n", - "\n", - "\n", - "def control_edge(state: State):\n", - " if state.get(\"status\") == \"success\":\n", - " return END\n", - " return \"solve\"\n", - "\n", - "\n", - "builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n", - "checkpointer = SqliteSaver.from_conn_string(\":memory:\")" - ] + "source": ["# This is all the same as before\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "markdown", @@ -1347,13 +825,7 @@ "id": "461c13ba-01cc-44e1-b837-6a64d03069d9", "metadata": {}, "outputs": [], - "source": [ - "graph = builder.compile(\n", - " checkpointer=checkpointer,\n", - " # New: this tells the graph to break any time it goes to the \"human\" node\n", - " interrupt_after=[\"evaluate\"],\n", - ")" - ] + "source": ["graph = builder.compile(\n checkpointer=checkpointer,\n # New: this tells the graph to break any time it goes to the \"human\" node\n interrupt_after=[\"evaluate\"],\n)"] }, { "cell_type": "code", @@ -1372,15 +844,7 @@ "output_type": "display_data" } ], - "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" - ] + "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"] }, { "cell_type": "markdown", @@ -1415,25 +879,7 @@ ] } ], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"silver-hl-1\", \"k\": 2}}\n", - "with tracing_v2_enabled(client=client):\n", - " events = graph.stream(silver_input, config)\n", - " for event in events:\n", - " for value in event.values():\n", - " messages = value.get(\"messages\")\n", - " if messages:\n", - " if isinstance(messages, list):\n", - " messages = value[\"messages\"][-1]\n", - " print(\n", - " \"Assistant:\",\n", - " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", - " )\n", - " elif value.get(\"examples\"):\n", - " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", - " elif value.get(\"candidate\"):\n", - " print(str(value[\"candidate\"].content)[:200])" - ] + "source": ["config = {\"configurable\": {\"thread_id\": \"silver-hl-1\", \"k\": 2}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(silver_input, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] }, { "cell_type": "markdown", @@ -1516,10 +962,7 @@ ] } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "print(snapshot.values[\"messages\"][0].content)" - ] + "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][0].content)"] }, { "cell_type": "markdown", @@ -1599,12 +1042,7 @@ ] } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "print(snapshot.values[\"messages\"][-2].content[0][\"text\"])\n", - "print(\"\\n\\nCode:\\n\\n\")\n", - "print(snapshot.values[\"messages\"][-2].tool_calls[0][\"args\"][\"code\"])" - ] + "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][-2].content[0][\"text\"])\nprint(\"\\n\\nCode:\\n\\n\")\nprint(snapshot.values[\"messages\"][-2].tool_calls[0][\"args\"][\"code\"])"] }, { "cell_type": "code", @@ -1633,9 +1071,7 @@ ] } ], - "source": [ - "print(snapshot.values[\"messages\"][-1].content[:200])" - ] + "source": ["print(snapshot.values[\"messages\"][-1].content[:200])"] }, { "cell_type": "markdown", @@ -1655,51 +1091,7 @@ "id": "b10fcbc9-6dd4-41ad-98f7-1cf1685035e6", "metadata": {}, "outputs": [], - "source": [ - "updated_config = graph.update_state(\n", - " config,\n", - " values={\n", - " \"messages\": [\n", - " (\n", - " \"user\",\n", - " \"\"\"Consider breaking down the algorithm into separate parts: reading inputs, detecting cycles using the tortoise and hare algorithm, and determining Bessie's final position by skipping ahead K steps.\n", - "\n", - "Read the inputs into three arrays:\n", - "- Two arrays L and R for the ports (adjust for 0-based indexing)\n", - "- A third array S for the direction sequence\n", - "\n", - "Optimize by multiplying K by M before the main loop to convert the number of repetitions into the total number of steps.\n", - "\n", - "Use the tortoise and hare algorithm to detect the cycle:\n", - "- Define a helper function get_next(v) that returns the next position and direction index\n", - "- Initialize two pointers s0 and s1 to (0, 0)\n", - "- In each iteration:\n", - " - Move s0 by 1 step and s1 by 2 steps using get_next()\n", - " - If s0 equals s1, decrement K by 1 and break out of the loop\n", - " - Otherwise, decrement K by 1\n", - "- After the loop, if K is not 0, there is a cycle\n", - "\n", - "To find the cycle length:\n", - "- Initialize a counter variable rho to 1\n", - "- Move s0 by 1 step using get_next()\n", - "- Enter a loop:\n", - " - Move s0 by 1 step using get_next()\n", - " - Increment rho\n", - " - If s0 equals s1, break out of the loop\n", - "\n", - "Skip ahead by reducing K modulo rho.\n", - "\n", - "Simulate the remaining steps:\n", - "- While K > 0, move s0 to the next position using get_next() and decrement K\n", - "\n", - "Print the final position (converted to 1-based indexing).\n", - "\n", - "Pay close attention to the initialization and movement of pointers during cycle detection and length calculation. Ensure that the logic is correct and handles all cases accurately.\"\"\",\n", - " )\n", - " ]\n", - " },\n", - ")" - ] + "source": ["updated_config = graph.update_state(\n config,\n values={\n \"messages\": [\n (\n \"user\",\n \"\"\"Consider breaking down the algorithm into separate parts: reading inputs, detecting cycles using the tortoise and hare algorithm, and determining Bessie's final position by skipping ahead K steps.\n\nRead the inputs into three arrays:\n- Two arrays L and R for the ports (adjust for 0-based indexing)\n- A third array S for the direction sequence\n\nOptimize by multiplying K by M before the main loop to convert the number of repetitions into the total number of steps.\n\nUse the tortoise and hare algorithm to detect the cycle:\n- Define a helper function get_next(v) that returns the next position and direction index\n- Initialize two pointers s0 and s1 to (0, 0)\n- In each iteration:\n - Move s0 by 1 step and s1 by 2 steps using get_next()\n - If s0 equals s1, decrement K by 1 and break out of the loop\n - Otherwise, decrement K by 1\n- After the loop, if K is not 0, there is a cycle\n\nTo find the cycle length:\n- Initialize a counter variable rho to 1\n- Move s0 by 1 step using get_next()\n- Enter a loop:\n - Move s0 by 1 step using get_next()\n - Increment rho\n - If s0 equals s1, break out of the loop\n\nSkip ahead by reducing K modulo rho.\n\nSimulate the remaining steps:\n- While K > 0, move s0 to the next position using get_next() and decrement K\n\nPrint the final position (converted to 1-based indexing).\n\nPay close attention to the initialization and movement of pointers during cycle detection and length calculation. Ensure that the logic is correct and handles all cases accurately.\"\"\",\n )\n ]\n },\n)"] }, { "cell_type": "markdown", @@ -1726,9 +1118,7 @@ "output_type": "execute_result" } ], - "source": [ - "graph.get_state(config).values[\"messages\"][-1]" - ] + "source": ["graph.get_state(config).values[\"messages\"][-1]"] }, { "cell_type": "markdown", @@ -1755,29 +1145,7 @@ ] } ], - "source": [ - "num_trials = 1\n", - "with tracing_v2_enabled(client=client):\n", - " for _ in range(num_trials):\n", - " events = graph.stream(None, updated_config)\n", - " for event in events:\n", - " for value in event.values():\n", - " messages = value.get(\"messages\")\n", - " if messages:\n", - " if isinstance(messages, list):\n", - " messages = value[\"messages\"][-1]\n", - " print(\n", - " \"Assistant:\",\n", - " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", - " )\n", - " elif value.get(\"examples\"):\n", - " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", - " elif value.get(\"candidate\"):\n", - " print(str(value[\"candidate\"].content)[:200])\n", - " if graph.get_state(config).values[\"status\"] == \"success\":\n", - " break\n", - " print(\"Continuing...\")" - ] + "source": ["num_trials = 1\nwith tracing_v2_enabled(client=client):\n for _ in range(num_trials):\n events = graph.stream(None, updated_config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])\n if graph.get_state(config).values[\"status\"] == \"success\":\n break\n print(\"Continuing...\")"] }, { "cell_type": "code", @@ -1785,9 +1153,7 @@ "id": "20ee7535-1bc8-4105-87c4-0e7a89a011ff", "metadata": {}, "outputs": [], - "source": [ - "most_recent_state = list(graph.get_state_history(config))[0]" - ] + "source": ["most_recent_state = list(graph.get_state_history(config))[0]"] }, { "cell_type": "markdown", @@ -1865,14 +1231,7 @@ ] } ], - "source": [ - "snapshot = graph.get_state(most_recent_state.config)\n", - "ai_message = snapshot.values[\"messages\"][-2]\n", - "if ai_message.content:\n", - " print(ai_message.content)\n", - "print(\"\\n\\nCode:\\n\\n\")\n", - "print(ai_message.tool_calls[0][\"args\"][\"code\"] if ai_message.tool_calls else \"N/A\")" - ] + "source": ["snapshot = graph.get_state(most_recent_state.config)\nai_message = snapshot.values[\"messages\"][-2]\nif ai_message.content:\n print(ai_message.content)\nprint(\"\\n\\nCode:\\n\\n\")\nprint(ai_message.tool_calls[0][\"args\"][\"code\"] if ai_message.tool_calls else \"N/A\")"] }, { "cell_type": "code", @@ -1903,9 +1262,7 @@ ] } ], - "source": [ - "print(snapshot.values[\"messages\"][-1].content[:200])" - ] + "source": ["print(snapshot.values[\"messages\"][-1].content[:200])"] }, { "cell_type": "markdown", @@ -1923,24 +1280,7 @@ "id": "6eb46517-cdd1-4716-8a9e-df72cdd9ba67", "metadata": {}, "outputs": [], - "source": [ - "updated_config = graph.update_state(\n", - " updated_config,\n", - " values={\n", - " \"messages\": [\n", - " (\n", - " \"user\",\n", - " \"\"\"That's better, but you're still getting some errors. Let's double check some things:\n", - " \n", - "1. When calculating the cycle length, make sure the initialization and movement of the pointers is correct. Double-check the logic there and see if you can spot any discrepancies.\n", - "2. Check the condition for whether there's a cycle after the main loop to ensure it covers all cases, like if K becomes 0 in the last iteration.\n", - "\n", - "Think step by step through youur implementation and update using the writePython tool.\"\"\",\n", - " )\n", - " ]\n", - " },\n", - ")" - ] + "source": ["updated_config = graph.update_state(\n updated_config,\n values={\n \"messages\": [\n (\n \"user\",\n \"\"\"That's better, but you're still getting some errors. Let's double check some things:\n \n1. When calculating the cycle length, make sure the initialization and movement of the pointers is correct. Double-check the logic there and see if you can spot any discrepancies.\n2. Check the condition for whether there's a cycle after the main loop to ensure it covers all cases, like if K becomes 0 in the last iteration.\n\nThink step by step through youur implementation and update using the writePython tool.\"\"\",\n )\n ]\n },\n)"] }, { "cell_type": "markdown", @@ -1964,29 +1304,7 @@ ] } ], - "source": [ - "num_trials = 2\n", - "with tracing_v2_enabled(client=client):\n", - " for _ in range(num_trials):\n", - " events = graph.stream(None, updated_config)\n", - " for event in events:\n", - " for value in event.values():\n", - " messages = value.get(\"messages\")\n", - " if messages:\n", - " if isinstance(messages, list):\n", - " messages = value[\"messages\"][-1]\n", - " print(\n", - " \"Assistant:\",\n", - " str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n", - " )\n", - " elif value.get(\"examples\"):\n", - " print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n", - " elif value.get(\"candidate\"):\n", - " print(str(value[\"candidate\"].content)[:200])\n", - " if graph.get_state(config).values[\"status\"] == \"success\":\n", - " break\n", - " print(\"Continuing...\")" - ] + "source": ["num_trials = 2\nwith tracing_v2_enabled(client=client):\n for _ in range(num_trials):\n events = graph.stream(None, updated_config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])\n if graph.get_state(config).values[\"status\"] == \"success\":\n break\n print(\"Continuing...\")"] }, { "cell_type": "markdown", @@ -2010,10 +1328,7 @@ ] } ], - "source": [ - "snapshot = graph.get_state(config)\n", - "print(snapshot.values[\"status\"])" - ] + "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"status\"])"] }, { "cell_type": "markdown", @@ -2046,7 +1361,7 @@ "id": "c71b4ba7-96ba-4643-b2de-eb2acdf4daba", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/visualization.ipynb b/examples/visualization.ipynb index 8d0e2f14f..19243b018 100644 --- a/examples/visualization.ipynb +++ b/examples/visualization.ipynb @@ -16,10 +16,7 @@ "id": "32a0e7f4", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph"] }, { "cell_type": "markdown", @@ -37,73 +34,7 @@ "id": "6d604311", "metadata": {}, "outputs": [], - "source": [ - "import random\n", - "from typing import Annotated, Literal\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.graph import StateGraph\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "\n", - "\n", - "class MyNode:\n", - " def __init__(self, name: str):\n", - " self.name = name\n", - "\n", - " def __call__(self, state: State):\n", - " return {\"messages\": [(\"assistant\", f\"Called node {self.name}\")]}\n", - "\n", - "\n", - "def route(state) -> Literal[\"entry_node\", \"__end__\"]:\n", - " if len(state[\"messages\"]) > 10:\n", - " return \"__end__\"\n", - " return \"entry_node\"\n", - "\n", - "\n", - "def add_fractal_nodes(builder, current_node, level, max_level):\n", - " if level > max_level:\n", - " return\n", - "\n", - " # Number of nodes to create at this level\n", - " num_nodes = random.randint(1, 3) # Adjust randomness as needed\n", - " for i in range(num_nodes):\n", - " nm = [\"A\", \"B\", \"C\"][i]\n", - " node_name = f\"node_{current_node}_{nm}\"\n", - " builder.add_node(node_name, MyNode(node_name))\n", - " builder.add_edge(current_node, node_name)\n", - "\n", - " # Recursively add more nodes\n", - " r = random.random()\n", - " if r > 0.2 and level + 1 < max_level:\n", - " add_fractal_nodes(builder, node_name, level + 1, max_level)\n", - " elif r > 0.05:\n", - " builder.add_conditional_edges(node_name, route, node_name)\n", - " else:\n", - " # End\n", - " builder.add_edge(node_name, \"__end__\")\n", - "\n", - "\n", - "def build_fractal_graph(max_level: int):\n", - " builder = StateGraph(State)\n", - " entry_point = \"entry_node\"\n", - " builder.add_node(entry_point, MyNode(entry_point))\n", - " builder.set_entry_point(entry_point)\n", - "\n", - " add_fractal_nodes(builder, entry_point, 1, max_level)\n", - "\n", - " # Optional: set a finish point if required\n", - " builder.set_finish_point(entry_point) # or any specific node\n", - "\n", - " return builder.compile()\n", - "\n", - "\n", - "app = build_fractal_graph(3)" - ] + "source": ["import random\nfrom typing import Annotated, Literal\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\nclass MyNode:\n def __init__(self, name: str):\n self.name = name\n\n def __call__(self, state: State):\n return {\"messages\": [(\"assistant\", f\"Called node {self.name}\")]}\n\n\ndef route(state) -> Literal[\"entry_node\", \"__end__\"]:\n if len(state[\"messages\"]) > 10:\n return \"__end__\"\n return \"entry_node\"\n\n\ndef add_fractal_nodes(builder, current_node, level, max_level):\n if level > max_level:\n return\n\n # Number of nodes to create at this level\n num_nodes = random.randint(1, 3) # Adjust randomness as needed\n for i in range(num_nodes):\n nm = [\"A\", \"B\", \"C\"][i]\n node_name = f\"node_{current_node}_{nm}\"\n builder.add_node(node_name, MyNode(node_name))\n builder.add_edge(current_node, node_name)\n\n # Recursively add more nodes\n r = random.random()\n if r > 0.2 and level + 1 < max_level:\n add_fractal_nodes(builder, node_name, level + 1, max_level)\n elif r > 0.05:\n builder.add_conditional_edges(node_name, route, node_name)\n else:\n # End\n builder.add_edge(node_name, \"__end__\")\n\n\ndef build_fractal_graph(max_level: int):\n builder = StateGraph(State)\n entry_point = \"entry_node\"\n builder.add_node(entry_point, MyNode(entry_point))\n builder.add_edge(START, entry_point)\n\n add_fractal_nodes(builder, entry_point, 1, max_level)\n\n # Optional: set a finish point if required\n builder.set_finish_point(entry_point) # or any specific node\n\n return builder.compile()\n\n\napp = build_fractal_graph(3)"] }, { "cell_type": "markdown", @@ -165,9 +96,7 @@ ] } ], - "source": [ - "app.get_graph().print_ascii()" - ] + "source": ["app.get_graph().print_ascii()"] }, { "cell_type": "markdown", @@ -226,9 +155,7 @@ ] } ], - "source": [ - "print(app.get_graph().draw_mermaid())" - ] + "source": ["print(app.get_graph().draw_mermaid())"] }, { "cell_type": "markdown", @@ -266,18 +193,7 @@ "output_type": "display_data" } ], - "source": [ - "from IPython.display import Image, display\n", - "from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeColors\n", - "\n", - "display(\n", - " Image(\n", - " app.get_graph().draw_mermaid_png(\n", - " draw_method=MermaidDrawMethod.API,\n", - " )\n", - " )\n", - ")" - ] + "source": ["from IPython.display import Image, display\nfrom langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeColors\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n draw_method=MermaidDrawMethod.API,\n )\n )\n)"] }, { "cell_type": "markdown", @@ -303,11 +219,7 @@ } }, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet pyppeteer\n", - "%pip install --quiet nest_asyncio" - ] + "source": ["%%capture --no-stderr\n%pip install --quiet pyppeteer\n%pip install --quiet nest_asyncio"] }, { "cell_type": "code", @@ -331,25 +243,7 @@ "output_type": "display_data" } ], - "source": [ - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n", - "\n", - "display(\n", - " Image(\n", - " app.get_graph().draw_mermaid_png(\n", - " curve_style=CurveStyle.LINEAR,\n", - " node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n", - " wrap_label_n_words=9,\n", - " output_file_path=None,\n", - " draw_method=MermaidDrawMethod.PYPPETEER,\n", - " background_color=\"white\",\n", - " padding=10,\n", - " )\n", - " )\n", - ")" - ] + "source": ["import nest_asyncio\n\nnest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n curve_style=CurveStyle.LINEAR,\n node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n wrap_label_n_words=9,\n output_file_path=None,\n draw_method=MermaidDrawMethod.PYPPETEER,\n background_color=\"white\",\n padding=10,\n )\n )\n)"] }, { "cell_type": "markdown", @@ -375,10 +269,7 @@ } }, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install pygraphviz" - ] + "source": ["%%capture --no-stderr\n%pip install pygraphviz"] }, { "cell_type": "code", @@ -402,9 +293,7 @@ "output_type": "display_data" } ], - "source": [ - "display(Image(app.get_graph().draw_png()))" - ] + "source": ["display(Image(app.get_graph().draw_png()))"] } ], "metadata": {