[Docs] Update notebooks to use START (#902)

This commit is contained in:
William FH
2024-07-01 21:36:34 -07:00
committed by GitHub
parent 267f5e5234
commit 727e63c01e
67 changed files with 1059 additions and 20258 deletions
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {
@@ -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": {