[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
+6 -11
View File
@@ -193,13 +193,7 @@ graph.add_edge("node_a", routing_function, {True: "node_b", False: "node_c"})
### Entry Point
The entry point is first node to call when the graph starts. You can use [`set_entry_point`][langgraph.graph.StateGraph.set_entry_point] to specify this.
```python
graph.set_entry_point("node_a")
```
This is equivalent to adding an edge between the `START` node and this node. You may want to use `START` directly when you want to have **multiple** nodes be called first.
The entry point is the first node(s) that are run when the graph starts. You can use the [`add_edge`][langgraph.graph.StateGraph.add_edge] method from the virtual [`START`][start] node to the first node to execute to specify where to enter the graph.
```python
from langgraph.graph import START
@@ -209,17 +203,18 @@ graph.add_edge(START, "node_a")
### Conditional Entry Point
The conditional entry point is used when you want to specify a function to call to determine which node(s) should be called first.
You can use [`set_conditional_entry_point`][langgraph.graph.StateGraph.set_conditional_entry_point] to specify this.
A conditional entry point lets you start at different nodes depending on custom logic. You can use [`add_conditional_edges`][langgraph.graph.StateGraph.add_conditional_edges] from the virtual [`START`][start] node to accomplish this.
```python
graph.set_conditional_entry_point(routing_function)
from langgraph.graph import START
graph.add_conditional_edges(START, routing_function)
```
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
```python
graph.set_conditional_entry_point(routing_function, {True: "node_b", False: "node_c"})
graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"})
```
## `Send`
+9 -140
View File
@@ -25,10 +25,7 @@
"id": "fdd4ce41-4152-423b-b3f7-be3b4d568cf4",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain langchain_openai langchainhub tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai langchainhub tavily-python"]
},
{
"cell_type": "markdown",
@@ -44,13 +41,7 @@
"id": "6398c4c1-da78-4595-8a5a-051ed2d1de72",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
"os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
]
"source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"]
},
{
"cell_type": "markdown",
@@ -66,10 +57,7 @@
"id": "dcbf79ad-4de5-43b0-a3a1-25b33711e46c",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -87,23 +75,7 @@
"id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078",
"metadata": {},
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain.agents import create_openai_functions_agent\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_openai.chat_models import ChatOpenAI\n",
"\n",
"tools = [TavilySearchResults(max_results=1)]\n",
"\n",
"# Get the prompt to use - you can modify this!\n",
"prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n",
"\n",
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n",
"\n",
"# Construct the OpenAI Functions agent\n",
"agent_runnable = create_openai_functions_agent(llm, tools, prompt)"
]
"source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"]
},
{
"cell_type": "markdown",
@@ -126,27 +98,7 @@
"id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict, Union\n",
"\n",
"from langchain_core.agents import AgentAction, AgentFinish\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" # The input string\n",
" input: str\n",
" # The list of previous messages in the conversation\n",
" chat_history: list[BaseMessage]\n",
" # The outcome of a given call to the agent\n",
" # Needs `None` as a valid type, since this is what this will start as\n",
" agent_outcome: Union[AgentAction, AgentFinish, None]\n",
" # List of actions and corresponding observations\n",
" # Here we annotate this with `operator.add` to indicate that operations to\n",
" # this state should be ADDED to the existing values (not overwrite it)\n",
" intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"]
},
{
"cell_type": "markdown",
@@ -181,42 +133,7 @@
"id": "d61a970d-edf4-4eef-9678-28bab7c72331",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.agents import AgentFinish\n",
"\n",
"from langgraph.prebuilt.tool_executor import ToolExecutor\n",
"\n",
"# This a helper class we have that is useful for running tools\n",
"# It takes in an agent action and calls that tool and returns the result\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"\n",
"# Define the agent\n",
"def run_agent(data):\n",
" agent_outcome = agent_runnable.invoke(data)\n",
" return {\"agent_outcome\": agent_outcome}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def execute_tools(data):\n",
" # Get the most recent agent_outcome - this is the key added in the `agent` above\n",
" agent_action = data[\"agent_outcome\"]\n",
" output = tool_executor.invoke(agent_action)\n",
" return {\"intermediate_steps\": [(agent_action, str(output))]}\n",
"\n",
"\n",
"# Define logic that will be used to determine which conditional edge to go down\n",
"def should_continue(data):\n",
" # If the agent outcome is an AgentFinish, then we return `exit` string\n",
" # This will be used when setting up the graph to define the flow\n",
" if isinstance(data[\"agent_outcome\"], AgentFinish):\n",
" return \"end\"\n",
" # Otherwise, an AgentAction is returned\n",
" # Here we return `continue` string\n",
" # This will be used when setting up the graph to define the flow\n",
" else:\n",
" return \"continue\""
]
"source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)\n\n\n# Define the agent\ndef run_agent(data):\n agent_outcome = agent_runnable.invoke(data)\n return {\"agent_outcome\": agent_outcome}\n\n\n# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -234,50 +151,7 @@
"id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", run_agent)\n",
"workflow.add_node(\"action\", execute_tools)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -300,12 +174,7 @@
]
}
],
"source": [
"inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n",
"for s in app.stream(inputs):\n",
" print(list(s.values())[0])\n",
" print(\"----\")"
]
"source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -313,7 +182,7 @@
"id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -30,10 +30,7 @@
"id": "694cfc4c-22a7-495d-930d-56b21d850ff9",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain langchain_openai tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"]
},
{
"cell_type": "markdown",
@@ -49,13 +46,7 @@
"id": "30c06a84-291a-4f58-9d31-53d3b56a3def",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
"os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
]
"source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"]
},
{
"cell_type": "markdown",
@@ -71,10 +62,7 @@
"id": "a8fb285a-7e6e-46fc-a273-43ab1a676189",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -92,23 +80,7 @@
"id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078",
"metadata": {},
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain.agents import create_openai_functions_agent\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_openai.chat_models import ChatOpenAI\n",
"\n",
"tools = [TavilySearchResults(max_results=1)]\n",
"\n",
"# Get the prompt to use - you can modify this!\n",
"prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n",
"\n",
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n",
"\n",
"# Construct the OpenAI Functions agent\n",
"agent_runnable = create_openai_functions_agent(llm, tools, prompt)"
]
"source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"]
},
{
"cell_type": "markdown",
@@ -131,27 +103,7 @@
"id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict, Union\n",
"\n",
"from langchain_core.agents import AgentAction, AgentFinish\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" # The input string\n",
" input: str\n",
" # The list of previous messages in the conversation\n",
" chat_history: list[BaseMessage]\n",
" # The outcome of a given call to the agent\n",
" # Needs `None` as a valid type, since this is what this will start as\n",
" agent_outcome: Union[AgentAction, AgentFinish, None]\n",
" # List of actions and corresponding observations\n",
" # Here we annotate this with `operator.add` to indicate that operations to\n",
" # this state should be ADDED to the existing values (not overwrite it)\n",
" intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"]
},
{
"cell_type": "markdown",
@@ -186,42 +138,7 @@
"id": "d61a970d-edf4-4eef-9678-28bab7c72331",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.agents import AgentFinish\n",
"\n",
"from langgraph.prebuilt.tool_executor import ToolExecutor\n",
"\n",
"# This a helper class we have that is useful for running tools\n",
"# It takes in an agent action and calls that tool and returns the result\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"\n",
"# Define the agent\n",
"def run_agent(data):\n",
" agent_outcome = agent_runnable.invoke(data)\n",
" return {\"agent_outcome\": agent_outcome}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def execute_tools(data):\n",
" # Get the most recent agent_outcome - this is the key added in the `agent` above\n",
" agent_action = data[\"agent_outcome\"]\n",
" output = tool_executor.invoke(agent_action)\n",
" return {\"intermediate_steps\": [(agent_action, str(output))]}\n",
"\n",
"\n",
"# Define logic that will be used to determine which conditional edge to go down\n",
"def should_continue(data):\n",
" # If the agent outcome is an AgentFinish, then we return `exit` string\n",
" # This will be used when setting up the graph to define the flow\n",
" if isinstance(data[\"agent_outcome\"], AgentFinish):\n",
" return \"end\"\n",
" # Otherwise, an AgentAction is returned\n",
" # Here we return `continue` string\n",
" # This will be used when setting up the graph to define the flow\n",
" else:\n",
" return \"continue\""
]
"source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)\n\n\n# Define the agent\ndef run_agent(data):\n agent_outcome = agent_runnable.invoke(data)\n return {\"agent_outcome\": agent_outcome}\n\n\n# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -250,9 +167,7 @@
"output_type": "execute_result"
}
],
"source": [
"tools[0].name"
]
"source": ["tools[0].name"]
},
{
"cell_type": "code",
@@ -260,21 +175,7 @@
"id": "df25d899-2338-4f31-a8bf-0582a2eec325",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.agents import AgentActionMessageLog\n",
"\n",
"\n",
"def first_agent(inputs):\n",
" action = AgentActionMessageLog(\n",
" # We force call this tool\n",
" tool=\"tavily_search_results_json\",\n",
" # We just pass in the `input` key to this tool\n",
" tool_input=inputs[\"input\"],\n",
" log=\"\",\n",
" message_log=[],\n",
" )\n",
" return {\"agent_outcome\": action}"
]
"source": ["from langchain_core.agents import AgentActionMessageLog\n\n\ndef first_agent(inputs):\n action = AgentActionMessageLog(\n # We force call this tool\n tool=\"tavily_search_results_json\",\n # We just pass in the `input` key to this tool\n tool_input=inputs[\"input\"],\n log=\"\",\n message_log=[],\n )\n return {\"agent_outcome\": action}"]
},
{
"cell_type": "markdown",
@@ -296,54 +197,7 @@
"id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", run_agent)\n",
"workflow.add_node(\"action\", execute_tools)\n",
"workflow.add_node(\"first_agent\", first_agent)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"first_agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# After the first agent, we want to take an action\n",
"workflow.add_edge(\"first_agent\", \"action\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\nworkflow.add_node(\"first_agent\", first_agent)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"first_agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After the first agent, we want to take an action\nworkflow.add_edge(\"first_agent\", \"action\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -366,12 +220,7 @@
]
}
],
"source": [
"inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n",
"for s in app.stream(inputs):\n",
" print(list(s.values())[0])\n",
" print(\"----\")"
]
"source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -379,7 +228,7 @@
"id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+10 -143
View File
@@ -30,10 +30,7 @@
"id": "3fa9e224-2f00-49e2-bca3-e9cb8d9f3d41",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain langchain_openai tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"]
},
{
"cell_type": "markdown",
@@ -49,13 +46,7 @@
"id": "d180f0d0-385f-4ce3-994c-11e1d64595b5",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
"os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
]
"source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"]
},
{
"cell_type": "markdown",
@@ -71,10 +62,7 @@
"id": "72ad0539-ecd8-4eb1-b2c1-2242e5fc556f",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -92,23 +80,7 @@
"id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078",
"metadata": {},
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain.agents import create_openai_functions_agent\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_openai.chat_models import ChatOpenAI\n",
"\n",
"tools = [TavilySearchResults(max_results=1)]\n",
"\n",
"# Get the prompt to use - you can modify this!\n",
"prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n",
"\n",
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n",
"\n",
"# Construct the OpenAI Functions agent\n",
"agent_runnable = create_openai_functions_agent(llm, tools, prompt)"
]
"source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"]
},
{
"cell_type": "markdown",
@@ -131,27 +103,7 @@
"id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict, Union\n",
"\n",
"from langchain_core.agents import AgentAction, AgentFinish\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" # The input string\n",
" input: str\n",
" # The list of previous messages in the conversation\n",
" chat_history: list[BaseMessage]\n",
" # The outcome of a given call to the agent\n",
" # Needs `None` as a valid type, since this is what this will start as\n",
" agent_outcome: Union[AgentAction, AgentFinish, None]\n",
" # List of actions and corresponding observations\n",
" # Here we annotate this with `operator.add` to indicate that operations to\n",
" # this state should be ADDED to the existing values (not overwrite it)\n",
" intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"]
},
{
"cell_type": "markdown",
@@ -186,21 +138,7 @@
"id": "2b757f84-1175-445e-8f8c-e5aeb765a03d",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.agents import AgentFinish\n",
"\n",
"from langgraph.prebuilt.tool_executor import ToolExecutor\n",
"\n",
"# This a helper class we have that is useful for running tools\n",
"# It takes in an agent action and calls that tool and returns the result\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"\n",
"# Define the agent\n",
"def run_agent(data):\n",
" agent_outcome = agent_runnable.invoke(data)\n",
" return {\"agent_outcome\": agent_outcome}"
]
"source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)\n\n\n# Define the agent\ndef run_agent(data):\n agent_outcome = agent_runnable.invoke(data)\n return {\"agent_outcome\": agent_outcome}"]
},
{
"cell_type": "markdown",
@@ -218,30 +156,7 @@
"id": "2fecf5e0-9604-4992-9c82-b9627466cd32",
"metadata": {},
"outputs": [],
"source": [
"# Define the function to execute tools\n",
"def execute_tools(data):\n",
" # Get the most recent agent_outcome - this is the key added in the `agent` above\n",
" agent_action = data[\"agent_outcome\"]\n",
" response = input(prompt=f\"[y/n] continue with: {agent_action}?\")\n",
" if response == \"n\":\n",
" raise ValueError\n",
" output = tool_executor.invoke(agent_action)\n",
" return {\"intermediate_steps\": [(agent_action, str(output))]}\n",
"\n",
"\n",
"# Define logic that will be used to determine which conditional edge to go down\n",
"def should_continue(data):\n",
" # If the agent outcome is an AgentFinish, then we return `exit` string\n",
" # This will be used when setting up the graph to define the flow\n",
" if isinstance(data[\"agent_outcome\"], AgentFinish):\n",
" return \"end\"\n",
" # Otherwise, an AgentAction is returned\n",
" # Here we return `continue` string\n",
" # This will be used when setting up the graph to define the flow\n",
" else:\n",
" return \"continue\""
]
"source": ["# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n response = input(prompt=f\"[y/n] continue with: {agent_action}?\")\n if response == \"n\":\n raise ValueError\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -259,50 +174,7 @@
"id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", run_agent)\n",
"workflow.add_node(\"action\", execute_tools)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -338,12 +210,7 @@
]
}
],
"source": [
"inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n",
"for s in app.stream(inputs):\n",
" print(list(s.values())[0])\n",
" print(\"----\")"
]
"source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -351,7 +218,7 @@
"id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -30,10 +30,7 @@
"id": "aa752131-27e3-4bd8-9f21-d6749a7e74f4",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain langchain_openai tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"]
},
{
"cell_type": "markdown",
@@ -49,13 +46,7 @@
"id": "5732e68f-4ae2-4db9-bf9c-454b4cc9ec01",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
"os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
]
"source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"]
},
{
"cell_type": "markdown",
@@ -71,10 +62,7 @@
"id": "652d4600-8f95-493f-b9b9-d4095aed9218",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -92,23 +80,7 @@
"id": "4499eb16-bca8-4a60-9a3a-2f34ae3f7078",
"metadata": {},
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain.agents import create_openai_functions_agent\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_openai.chat_models import ChatOpenAI\n",
"\n",
"tools = [TavilySearchResults(max_results=1)]\n",
"\n",
"# Get the prompt to use - you can modify this!\n",
"prompt = hub.pull(\"hwchase17/openai-functions-agent\")\n",
"\n",
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n",
"\n",
"# Construct the OpenAI Functions agent\n",
"agent_runnable = create_openai_functions_agent(llm, tools, prompt)"
]
"source": ["from langchain import hub\nfrom langchain.agents import create_openai_functions_agent\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_openai.chat_models import ChatOpenAI\n\ntools = [TavilySearchResults(max_results=1)]\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"hwchase17/openai-functions-agent\")\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-1106\", streaming=True)\n\n# Construct the OpenAI Functions agent\nagent_runnable = create_openai_functions_agent(llm, tools, prompt)"]
},
{
"cell_type": "markdown",
@@ -131,27 +103,7 @@
"id": "c941fb10-dbe5-4d6a-ab7d-133d01c33cc4",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict, Union\n",
"\n",
"from langchain_core.agents import AgentAction, AgentFinish\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" # The input string\n",
" input: str\n",
" # The list of previous messages in the conversation\n",
" chat_history: list[BaseMessage]\n",
" # The outcome of a given call to the agent\n",
" # Needs `None` as a valid type, since this is what this will start as\n",
" agent_outcome: Union[AgentAction, AgentFinish, None]\n",
" # List of actions and corresponding observations\n",
" # Here we annotate this with `operator.add` to indicate that operations to\n",
" # this state should be ADDED to the existing values (not overwrite it)\n",
" intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict, Union\n\nfrom langchain_core.agents import AgentAction, AgentFinish\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n # The input string\n input: str\n # The list of previous messages in the conversation\n chat_history: list[BaseMessage]\n # The outcome of a given call to the agent\n # Needs `None` as a valid type, since this is what this will start as\n agent_outcome: Union[AgentAction, AgentFinish, None]\n # List of actions and corresponding observations\n # Here we annotate this with `operator.add` to indicate that operations to\n # this state should be ADDED to the existing values (not overwrite it)\n intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add]"]
},
{
"cell_type": "markdown",
@@ -186,15 +138,7 @@
"id": "77e3c059-e31f-4c8f-81bf-edb58688e12b",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.agents import AgentFinish\n",
"\n",
"from langgraph.prebuilt.tool_executor import ToolExecutor\n",
"\n",
"# This a helper class we have that is useful for running tools\n",
"# It takes in an agent action and calls that tool and returns the result\n",
"tool_executor = ToolExecutor(tools)"
]
"source": ["from langchain_core.agents import AgentFinish\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor\n\n# This a helper class we have that is useful for running tools\n# It takes in an agent action and calls that tool and returns the result\ntool_executor = ToolExecutor(tools)"]
},
{
"cell_type": "markdown",
@@ -212,36 +156,7 @@
"id": "a9f66a3e-aba1-4893-95b1-a433c7091d5e",
"metadata": {},
"outputs": [],
"source": [
"# Define the agent\n",
"def run_agent(data):\n",
" inputs = data.copy()\n",
" if len(inputs[\"intermediate_steps\"]) > 5:\n",
" inputs[\"intermediate_steps\"] = inputs[\"intermediate_steps\"][-5:]\n",
" agent_outcome = agent_runnable.invoke(inputs)\n",
" return {\"agent_outcome\": agent_outcome}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def execute_tools(data):\n",
" # Get the most recent agent_outcome - this is the key added in the `agent` above\n",
" agent_action = data[\"agent_outcome\"]\n",
" output = tool_executor.invoke(agent_action)\n",
" return {\"intermediate_steps\": [(agent_action, str(output))]}\n",
"\n",
"\n",
"# Define logic that will be used to determine which conditional edge to go down\n",
"def should_continue(data):\n",
" # If the agent outcome is an AgentFinish, then we return `exit` string\n",
" # This will be used when setting up the graph to define the flow\n",
" if isinstance(data[\"agent_outcome\"], AgentFinish):\n",
" return \"end\"\n",
" # Otherwise, an AgentAction is returned\n",
" # Here we return `continue` string\n",
" # This will be used when setting up the graph to define the flow\n",
" else:\n",
" return \"continue\""
]
"source": ["# Define the agent\ndef run_agent(data):\n inputs = data.copy()\n if len(inputs[\"intermediate_steps\"]) > 5:\n inputs[\"intermediate_steps\"] = inputs[\"intermediate_steps\"][-5:]\n agent_outcome = agent_runnable.invoke(inputs)\n return {\"agent_outcome\": agent_outcome}\n\n\n# Define the function to execute tools\ndef execute_tools(data):\n # Get the most recent agent_outcome - this is the key added in the `agent` above\n agent_action = data[\"agent_outcome\"]\n output = tool_executor.invoke(agent_action)\n return {\"intermediate_steps\": [(agent_action, str(output))]}\n\n\n# Define logic that will be used to determine which conditional edge to go down\ndef should_continue(data):\n # If the agent outcome is an AgentFinish, then we return `exit` string\n # This will be used when setting up the graph to define the flow\n if isinstance(data[\"agent_outcome\"], AgentFinish):\n return \"end\"\n # Otherwise, an AgentAction is returned\n # Here we return `continue` string\n # This will be used when setting up the graph to define the flow\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -259,50 +174,7 @@
"id": "c4054dde-4618-49b7-998a-daa0c1d6d6c0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", run_agent)\n",
"workflow.add_node(\"action\", execute_tools)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", run_agent)\nworkflow.add_node(\"action\", execute_tools)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -325,12 +197,7 @@
]
}
],
"source": [
"inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\n",
"for s in app.stream(inputs):\n",
" print(list(s.values())[0])\n",
" print(\"----\")"
]
"source": ["inputs = {\"input\": \"what is the weather in sf\", \"chat_history\": []}\nfor s in app.stream(inputs):\n print(list(s.values())[0])\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -338,7 +205,7 @@
"id": "2eb662bc-de7d-4a57-a3e8-2f00dcf4ff8b",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+15 -165
View File
@@ -37,10 +37,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
},
{
"cell_type": "markdown",
@@ -56,18 +53,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -83,10 +69,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -112,22 +95,7 @@
"id": "6768a3ab",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# Add messages essentially does this with more\n",
"# robust handling\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
},
{
"cell_type": "markdown",
@@ -147,19 +115,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder, but don't tell the LLM that...\n",
" return [\"The answer to your question lies within.\"]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -176,11 +132,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -204,11 +156,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-haiku-20240307\")"
]
"source": ["from langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-haiku-20240307\")"]
},
{
"cell_type": "markdown",
@@ -226,9 +174,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -267,29 +213,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state: State) -> Literal[\"end\", \"continue\"]:\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no tool call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"async def call_model(state: State):\n",
" messages = state[\"messages\"]\n",
" response = await model.ainvoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}"
]
"source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"end\", \"continue\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no tool call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\nasync def call_model(state: State):\n messages = state[\"messages\"]\n response = await model.ainvoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"]
},
{
"cell_type": "markdown",
@@ -307,50 +231,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -369,11 +250,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -406,12 +283,7 @@
"output_type": "execute_result"
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"await app.ainvoke(inputs)"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nawait app.ainvoke(inputs)"]
},
{
"cell_type": "markdown",
@@ -480,16 +352,7 @@
]
}
],
"source": [
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"async for output in app.astream(inputs, stream_mode=\"updates\"):\n",
" # stream_mode=\"updates\" yields dictionaries with output keyed by node name\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value[\"messages\"][-1].pretty_print())\n",
" print(\"\\n---\\n\")"
]
"source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nasync for output in app.astream(inputs, stream_mode=\"updates\"):\n # stream_mode=\"updates\" yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value[\"messages\"][-1].pretty_print())\n print(\"\\n---\\n\")"]
},
{
"cell_type": "markdown",
@@ -546,20 +409,7 @@
]
}
],
"source": [
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n",
" # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n",
" for op in output.ops:\n",
" if op[\"path\"] == \"/streamed_output/-\":\n",
" # this is the output from .stream()\n",
" ...\n",
" elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n",
" \"/streamed_output/-\"\n",
" ):\n",
" # because we chose to only include LLMs, these are LLM tokens\n",
" print(op[\"value\"].content, end=\"|\")"
]
"source": ["inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nasync for output in app.astream_log(inputs, include_types=[\"llm\"]):\n # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n for op in output.ops:\n if op[\"path\"] == \"/streamed_output/-\":\n # this is the output from .stream()\n ...\n elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n \"/streamed_output/-\"\n ):\n # because we chose to only include LLMs, these are LLM tokens\n print(op[\"value\"].content, end=\"|\")"]
},
{
"cell_type": "code",
@@ -567,7 +417,7 @@
"id": "08ae8246-11d5-40e1-8567-361e5bef8917",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+15 -235
View File
@@ -20,10 +20,7 @@
"id": "bb54e2d0",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph"]
},
{
"cell_type": "markdown",
@@ -39,42 +36,7 @@
"id": "09372b8b-edea-4b9d-9ec3-3d93ce1ba819",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Any\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph\n",
"\n",
"\n",
"class State(TypedDict):\n",
" # The operator.add reducer fn makes this append-only\n",
" aggregate: Annotated[list, operator.add]\n",
"\n",
"\n",
"class ReturnNodeValue:\n",
" def __init__(self, node_secret: str):\n",
" self._value = node_secret\n",
"\n",
" def __call__(self, state: State) -> Any:\n",
" print(f\"Adding {self._value} to {state['aggregate']}\")\n",
" return {\"aggregate\": [self._value]}\n",
"\n",
"\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
"builder.set_entry_point(\"a\")\n",
"builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
"builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
"builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
"builder.add_edge(\"a\", \"b\")\n",
"builder.add_edge(\"a\", \"c\")\n",
"builder.add_edge(\"b\", \"d\")\n",
"builder.add_edge(\"c\", \"d\")\n",
"builder.set_finish_point(\"d\")\n",
"graph = builder.compile()"
]
"source": ["import operator\nfrom typing import Annotated, Any\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n\n\nclass ReturnNodeValue:\n def __init__(self, node_secret: str):\n self._value = node_secret\n\n def __call__(self, state: State) -> Any:\n print(f\"Adding {self._value} to {state['aggregate']}\")\n return {\"aggregate\": [self._value]}\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_edge(\"a\", \"b\")\nbuilder.add_edge(\"a\", \"c\")\nbuilder.add_edge(\"b\", \"d\")\nbuilder.add_edge(\"c\", \"d\")\nbuilder.set_finish_point(\"d\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -93,11 +55,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(graph.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -126,9 +84,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})"
]
"source": ["graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})"]
},
{
"cell_type": "markdown",
@@ -162,34 +118,7 @@
"id": "259a7704-5aa0-4e4c-aeef-cca04e8be0ff",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph\n",
"\n",
"\n",
"class State(TypedDict):\n",
" # The operator.add reducer fn makes this append-only\n",
" aggregate: Annotated[list, operator.add]\n",
"\n",
"\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
"builder.set_entry_point(\"a\")\n",
"builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
"builder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\n",
"builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
"builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
"builder.add_edge(\"a\", \"b\")\n",
"builder.add_edge(\"a\", \"c\")\n",
"builder.add_edge(\"b\", \"b2\")\n",
"builder.add_edge([\"b2\", \"c\"], \"d\")\n",
"builder.set_finish_point(\"d\")\n",
"graph = builder.compile()"
]
"source": ["import operator\nfrom typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_edge(\"a\", \"b\")\nbuilder.add_edge(\"a\", \"c\")\nbuilder.add_edge(\"b\", \"b2\")\nbuilder.add_edge([\"b2\", \"c\"], \"d\")\nbuilder.set_finish_point(\"d\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -208,11 +137,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(graph.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -242,9 +167,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"aggregate\": []})"
]
"source": ["graph.invoke({\"aggregate\": []})"]
},
{
"cell_type": "markdown",
@@ -264,49 +187,7 @@
"id": "95f5e026",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import END, START, StateGraph\n",
"\n",
"\n",
"class State(TypedDict):\n",
" # The operator.add reducer fn makes this append-only\n",
" aggregate: Annotated[list, operator.add]\n",
" which: str\n",
"\n",
"\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
"builder.add_edge(START, \"a\")\n",
"builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
"builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
"builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
"builder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n",
"\n",
"\n",
"def route_bc_or_cd(state: State) -> Sequence[str]:\n",
" if state[\"which\"] == \"cd\":\n",
" return [\"c\", \"d\"]\n",
" return [\"b\", \"c\"]\n",
"\n",
"\n",
"intermediates = [\"b\", \"c\", \"d\"]\n",
"builder.add_conditional_edges(\n",
" \"a\",\n",
" route_bc_or_cd,\n",
" intermediates,\n",
")\n",
"for node in intermediates:\n",
" builder.add_edge(node, \"e\")\n",
"\n",
"\n",
"builder.add_edge(\"e\", END)\n",
"graph = builder.compile()"
]
"source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, START, StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n which: str\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n\n\ndef route_bc_or_cd(state: State) -> Sequence[str]:\n if state[\"which\"] == \"cd\":\n return [\"c\", \"d\"]\n return [\"b\", \"c\"]\n\n\nintermediates = [\"b\", \"c\", \"d\"]\nbuilder.add_conditional_edges(\n \"a\",\n route_bc_or_cd,\n intermediates,\n)\nfor node in intermediates:\n builder.add_edge(node, \"e\")\n\n\nbuilder.add_edge(\"e\", END)\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -325,11 +206,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(graph.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -358,9 +235,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"aggregate\": [], \"which\": \"bc\"})"
]
"source": ["graph.invoke({\"aggregate\": [], \"which\": \"bc\"})"]
},
{
"cell_type": "code",
@@ -389,9 +264,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"
]
"source": ["graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"]
},
{
"cell_type": "markdown",
@@ -413,92 +286,7 @@
"id": "836bc12d",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph\n",
"\n",
"\n",
"def reduce_fanouts(left, right):\n",
" if left is None:\n",
" left = []\n",
" if not right:\n",
" # Overwrite\n",
" return []\n",
" return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" # The operator.add reducer fn makes this append-only\n",
" aggregate: Annotated[list, operator.add]\n",
" fanout_values: Annotated[list, reduce_fanouts]\n",
" which: str\n",
"\n",
"\n",
"builder = StateGraph(State)\n",
"builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
"builder.set_entry_point(\"a\")\n",
"\n",
"\n",
"class ParallelReturnNodeValue:\n",
" def __init__(\n",
" self,\n",
" node_secret: str,\n",
" reliability: float,\n",
" ):\n",
" self._value = node_secret\n",
" self._reliability = reliability\n",
"\n",
" def __call__(self, state: State) -> Any:\n",
" print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n",
" return {\n",
" \"fanout_values\": [\n",
" {\n",
" \"value\": [self._value],\n",
" \"reliability\": self._reliability,\n",
" }\n",
" ]\n",
" }\n",
"\n",
"\n",
"builder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.9))\n",
"\n",
"builder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.1))\n",
"builder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.3))\n",
"\n",
"\n",
"def aggregate_fanout_values(state: State) -> Any:\n",
" # Sort by reliability\n",
" ranked_values = sorted(\n",
" state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n",
" )\n",
" return {\n",
" \"aggregate\": [x[\"value\"] for x in ranked_values] + [\"I'm E\"],\n",
" \"fanout_values\": [],\n",
" }\n",
"\n",
"\n",
"builder.add_node(\"e\", aggregate_fanout_values)\n",
"\n",
"\n",
"def route_bc_or_cd(state: State) -> Sequence[str]:\n",
" if state[\"which\"] == \"cd\":\n",
" return [\"c\", \"d\"]\n",
" return [\"b\", \"c\"]\n",
"\n",
"\n",
"intermediates = [\"b\", \"c\", \"d\"]\n",
"builder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n",
"\n",
"for node in intermediates:\n",
" builder.add_edge(node, \"e\")\n",
"\n",
"builder.set_finish_point(\"e\")\n",
"graph = builder.compile()"
]
"source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\ndef reduce_fanouts(left, right):\n if left is None:\n left = []\n if not right:\n # Overwrite\n return []\n return left + right\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n fanout_values: Annotated[list, reduce_fanouts]\n which: str\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\n\n\nclass ParallelReturnNodeValue:\n def __init__(\n self,\n node_secret: str,\n reliability: float,\n ):\n self._value = node_secret\n self._reliability = reliability\n\n def __call__(self, state: State) -> Any:\n print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n return {\n \"fanout_values\": [\n {\n \"value\": [self._value],\n \"reliability\": self._reliability,\n }\n ]\n }\n\n\nbuilder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.9))\n\nbuilder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.1))\nbuilder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.3))\n\n\ndef aggregate_fanout_values(state: State) -> Any:\n # Sort by reliability\n ranked_values = sorted(\n state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n )\n return {\n \"aggregate\": [x[\"value\"] for x in ranked_values] + [\"I'm E\"],\n \"fanout_values\": [],\n }\n\n\nbuilder.add_node(\"e\", aggregate_fanout_values)\n\n\ndef route_bc_or_cd(state: State) -> Sequence[str]:\n if state[\"which\"] == \"cd\":\n return [\"c\", \"d\"]\n return [\"b\", \"c\"]\n\n\nintermediates = [\"b\", \"c\", \"d\"]\nbuilder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n\nfor node in intermediates:\n builder.add_edge(node, \"e\")\n\nbuilder.set_finish_point(\"e\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -517,11 +305,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(graph.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -551,9 +335,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})"
]
"source": ["graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})"]
},
{
"cell_type": "code",
@@ -583,9 +365,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"
]
"source": ["graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"]
}
],
"metadata": {
@@ -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": {
@@ -26,10 +26,7 @@
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
"metadata": {},
"outputs": [],
"source": [
"# %%capture --no-stderr\n",
"# %pip install -U langgraph langchain langchain_openai"
]
"source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai"]
},
{
"cell_type": "code",
@@ -37,24 +34,7 @@
"id": "30c2f3de-c730-4aec-85a6-af2c2f058803",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"\n",
"# Optional, add tracing in LangSmith.\n",
"# This will help you visualize and debug the control flow\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""]
},
{
"cell_type": "markdown",
@@ -75,24 +55,7 @@
"id": "828479af-cf9c-4888-a365-599643a96b55",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"import openai\n",
"\n",
"\n",
"# This is flexible, but you can define your agent here, or call your agent API here.\n",
"def my_chat_bot(messages: List[dict]) -> dict:\n",
" system_message = {\n",
" \"role\": \"system\",\n",
" \"content\": \"You are a customer support agent for an airline.\",\n",
" }\n",
" messages = [system_message] + messages\n",
" completion = openai.chat.completions.create(\n",
" messages=messages, model=\"gpt-3.5-turbo\"\n",
" )\n",
" return completion.choices[0].message.model_dump()"
]
"source": ["from typing import List\n\nimport openai\n\n\n# This is flexible, but you can define your agent here, or call your agent API here.\ndef my_chat_bot(messages: List[dict]) -> dict:\n system_message = {\n \"role\": \"system\",\n \"content\": \"You are a customer support agent for an airline.\",\n }\n messages = [system_message] + messages\n completion = openai.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\"\n )\n return completion.choices[0].message.model_dump()"]
},
{
"cell_type": "code",
@@ -114,9 +77,7 @@
"output_type": "execute_result"
}
],
"source": [
"my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"
]
"source": ["my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"]
},
{
"cell_type": "markdown",
@@ -135,33 +96,7 @@
"id": "32c147df-7f90-4b0d-9a6b-671677020353",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"system_prompt_template = \"\"\"You are a customer of an airline company. \\\n",
"You are interacting with a user who is a customer support person. \\\n",
"\n",
"{instructions}\n",
"\n",
"When you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt_template),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"instructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\n",
"You want them to give you ALL the money back. \\\n",
"This trip happened 5 years ago.\"\"\"\n",
"\n",
"prompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n",
"\n",
"model = ChatOpenAI()\n",
"\n",
"simulated_user = prompt | model"
]
"source": ["from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nsystem_prompt_template = \"\"\"You are a customer of an airline company. \\\nYou are interacting with a user who is a customer support person. \\\n\n{instructions}\n\nWhen you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt_template),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\ninstructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\nYou want them to give you ALL the money back. \\\nThis trip happened 5 years ago.\"\"\"\n\nprompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n\nmodel = ChatOpenAI()\n\nsimulated_user = prompt | model"]
},
{
"cell_type": "code",
@@ -180,12 +115,7 @@
"output_type": "execute_result"
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"messages = [HumanMessage(content=\"Hi! How can I help you?\")]\n",
"simulated_user.invoke({\"messages\": messages})"
]
"source": ["from langchain_core.messages import HumanMessage\n\nmessages = [HumanMessage(content=\"Hi! How can I help you?\")]\nsimulated_user.invoke({\"messages\": messages})"]
},
{
"cell_type": "markdown",
@@ -223,19 +153,7 @@
"id": "69e2a3a3-40f3-4223-9136-113738440be9",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.adapters.openai import convert_message_to_dict\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"def chat_bot_node(messages):\n",
" # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n",
" messages = [convert_message_to_dict(m) for m in messages]\n",
" # Call the chat bot\n",
" chat_bot_response = my_chat_bot(messages)\n",
" # Respond with an AI Message\n",
" return AIMessage(content=chat_bot_response[\"content\"])"
]
"source": ["from langchain_community.adapters.openai import convert_message_to_dict\nfrom langchain_core.messages import AIMessage\n\n\ndef chat_bot_node(messages):\n # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n messages = [convert_message_to_dict(m) for m in messages]\n # Call the chat bot\n chat_bot_response = my_chat_bot(messages)\n # Respond with an AI Message\n return AIMessage(content=chat_bot_response[\"content\"])"]
},
{
"cell_type": "markdown",
@@ -251,25 +169,7 @@
"id": "7cad7527-ffa5-4c30-8585-b54a7a18bd98",
"metadata": {},
"outputs": [],
"source": [
"def _swap_roles(messages):\n",
" new_messages = []\n",
" for m in messages:\n",
" if isinstance(m, AIMessage):\n",
" new_messages.append(HumanMessage(content=m.content))\n",
" else:\n",
" new_messages.append(AIMessage(content=m.content))\n",
" return new_messages\n",
"\n",
"\n",
"def simulated_user_node(messages):\n",
" # Swap roles of messages\n",
" new_messages = _swap_roles(messages)\n",
" # Call the simulated user\n",
" response = simulated_user.invoke({\"messages\": new_messages})\n",
" # This response is an AI message - we need to flip this to be a human message\n",
" return HumanMessage(content=response.content)"
]
"source": ["def _swap_roles(messages):\n new_messages = []\n for m in messages:\n if isinstance(m, AIMessage):\n new_messages.append(HumanMessage(content=m.content))\n else:\n new_messages.append(AIMessage(content=m.content))\n return new_messages\n\n\ndef simulated_user_node(messages):\n # Swap roles of messages\n new_messages = _swap_roles(messages)\n # Call the simulated user\n response = simulated_user.invoke({\"messages\": new_messages})\n # This response is an AI message - we need to flip this to be a human message\n return HumanMessage(content=response.content)"]
},
{
"cell_type": "markdown",
@@ -292,15 +192,7 @@
"id": "28004fbf-a2f3-46b7-bde7-46c7adaf97fb",
"metadata": {},
"outputs": [],
"source": [
"def should_continue(messages):\n",
" if len(messages) > 6:\n",
" return \"end\"\n",
" elif messages[-1].content == \"FINISHED\":\n",
" return \"end\"\n",
" else:\n",
" return \"continue\""
]
"source": ["def should_continue(messages):\n if len(messages) > 6:\n return \"end\"\n elif messages[-1].content == \"FINISHED\":\n return \"end\"\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -318,29 +210,7 @@
"id": "0b597e4b-4cbb-4bbc-82e5-f7e31275964c",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, MessageGraph\n",
"\n",
"graph_builder = MessageGraph()\n",
"graph_builder.add_node(\"user\", simulated_user_node)\n",
"graph_builder.add_node(\"chat_bot\", chat_bot_node)\n",
"# Every response from your chat bot will automatically go to the\n",
"# simulated user\n",
"graph_builder.add_edge(\"chat_bot\", \"user\")\n",
"graph_builder.add_conditional_edges(\n",
" \"user\",\n",
" should_continue,\n",
" # If the finish criteria are met, we will stop the simulation,\n",
" # otherwise, the virtual user's message will be sent to your chat bot\n",
" {\n",
" \"end\": END,\n",
" \"continue\": \"chat_bot\",\n",
" },\n",
")\n",
"# The input will first go to your chat bot\n",
"graph_builder.set_entry_point(\"chat_bot\")\n",
"simulation = graph_builder.compile()"
]
"source": ["from langgraph.graph import END, MessageGraph, START\n\ngraph_builder = MessageGraph()\ngraph_builder.add_node(\"user\", simulated_user_node)\ngraph_builder.add_node(\"chat_bot\", chat_bot_node)\n# Every response from your chat bot will automatically go to the\n# simulated user\ngraph_builder.add_edge(\"chat_bot\", \"user\")\ngraph_builder.add_conditional_edges(\n \"user\",\n should_continue,\n # If the finish criteria are met, we will stop the simulation,\n # otherwise, the virtual user's message will be sent to your chat bot\n {\n \"end\": END,\n \"continue\": \"chat_bot\",\n },\n)\n# The input will first go to your chat bot\ngraph_builder.add_edge(START, \"chat_bot\")\nsimulation = graph_builder.compile()"]
},
{
"cell_type": "markdown",
@@ -381,13 +251,7 @@
]
}
],
"source": [
"for chunk in simulation.stream([]):\n",
" # Print out all events aside from the final end chunk\n",
" if END not in chunk:\n",
" print(chunk)\n",
" print(\"----\")"
]
"source": ["for chunk in simulation.stream([]):\n # Print out all events aside from the final end chunk\n if END not in chunk:\n print(chunk)\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -395,7 +259,7 @@
"id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -9,7 +9,7 @@ from langchain_core.runnables import chain as as_runnable
from langchain_openai import ChatOpenAI
from typing_extensions import TypedDict
from langgraph.graph import END, StateGraph
from langgraph.graph import END, StateGraph, START
def langchain_to_openai_messages(messages: List[BaseMessage]):
@@ -116,7 +116,7 @@ def create_chat_simulator(
should_continue or functools.partial(_should_continue, max_turns=max_turns),
)
# If your dataset has a 'leading question/input', then we route first to the assistant, otherwise, we let the user take the lead.
graph_builder.set_entry_point("assistant" if input_key is not None else "user")
graph_builder.add_edge(START, "assistant" if input_key is not None else "user")
return (
RunnableLambda(_prepare_example).bind(input_key=input_key)
@@ -34,9 +34,7 @@
"id": "e3900420",
"metadata": {},
"outputs": [],
"source": [
"! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4"
]
"source": ["! pip install -U langchain_community langchain-openai langchain-anthropic langchain langgraph bs4"]
},
{
"cell_type": "markdown",
@@ -54,24 +52,7 @@
"id": "c2eb35d1-4990-47dc-a5c4-208bae588a82",
"metadata": {},
"outputs": [],
"source": [
"from bs4 import BeautifulSoup as Soup\n",
"from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n",
"\n",
"# LCEL docs\n",
"url = \"https://python.langchain.com/v0.2/docs/concepts/#langchain-expression-language-lcel\"\n",
"loader = RecursiveUrlLoader(\n",
" url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n",
")\n",
"docs = loader.load()\n",
"\n",
"# Sort the list based on the URLs and get the text\n",
"d_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\n",
"d_reversed = list(reversed(d_sorted))\n",
"concatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n",
" [doc.page_content for doc in d_reversed]\n",
")"
]
"source": ["from bs4 import BeautifulSoup as Soup\nfrom langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n\n# LCEL docs\nurl = \"https://python.langchain.com/v0.2/docs/concepts/#langchain-expression-language-lcel\"\nloader = RecursiveUrlLoader(\n url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n)\ndocs = loader.load()\n\n# Sort the list based on the URLs and get the text\nd_sorted = sorted(docs, key=lambda x: x.metadata[\"source\"])\nd_reversed = list(reversed(d_sorted))\nconcatenated_content = \"\\n\\n\\n --- \\n\\n\\n\".join(\n [doc.page_content for doc in d_reversed]\n)"]
},
{
"cell_type": "markdown",
@@ -93,45 +74,7 @@
"id": "3ba3df70-f6b4-4ea5-a210-e10944960bc6",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"### OpenAI\n",
"\n",
"# Grader prompt\n",
"code_gen_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n",
" Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n",
" question based on the above provided documentation. Ensure any code you provide can be executed \\n \n",
" with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n",
" Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"# Data model\n",
"class code(BaseModel):\n",
" \"\"\"Code output\"\"\"\n",
"\n",
" prefix: str = Field(description=\"Description of the problem and approach\")\n",
" imports: str = Field(description=\"Code block import statements\")\n",
" code: str = Field(description=\"Code block not including import statements\")\n",
" description = \"Schema for code solutions to questions about LCEL.\"\n",
"\n",
"\n",
"expt_llm = \"gpt-4-0125-preview\"\n",
"llm = ChatOpenAI(temperature=0, model=expt_llm)\n",
"code_gen_chain = code_gen_prompt | llm.with_structured_output(code)\n",
"question = \"How do I build a RAG chain in LCEL?\"\n",
"# solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})"
]
"source": ["from langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n### OpenAI\n\n# Grader prompt\ncode_gen_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n Here is a full set of LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user \n question based on the above provided documentation. Ensure any code you provide can be executed \\n \n with all required imports and variables defined. Structure your answer with a description of the code solution. \\n\n Then list the imports. And finally list the functioning code block. Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\nexpt_llm = \"gpt-4-0125-preview\"\nllm = ChatOpenAI(temperature=0, model=expt_llm)\ncode_gen_chain = code_gen_prompt | llm.with_structured_output(code)\nquestion = \"How do I build a RAG chain in LCEL?\"\n# solution = code_gen_chain_oai.invoke({\"context\":concatenated_content,\"messages\":[(\"user\",question)]})"]
},
{
"cell_type": "code",
@@ -139,118 +82,7 @@
"id": "cd30b67d-96db-4e51-a540-ae23fcc1f878",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"### Anthropic\n",
"\n",
"# Prompt to enforce tool use\n",
"code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"<instructions> You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n",
" Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n",
" above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n",
" defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n",
" Invoke the code tool to structure the output correctly. </instructions> \\n Here is the user question:\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"# Data model\n",
"class code(BaseModel):\n",
" \"\"\"Code output\"\"\"\n",
"\n",
" prefix: str = Field(description=\"Description of the problem and approach\")\n",
" imports: str = Field(description=\"Code block import statements\")\n",
" code: str = Field(description=\"Code block not including import statements\")\n",
" description = \"Schema for code solutions to questions about LCEL.\"\n",
"\n",
"\n",
"# LLM\n",
"# expt_llm = \"claude-3-haiku-20240307\"\n",
"expt_llm = \"claude-3-opus-20240229\"\n",
"llm = ChatAnthropic(\n",
" model=expt_llm,\n",
" default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n",
")\n",
"\n",
"structured_llm_claude = llm.with_structured_output(code, include_raw=True)\n",
"\n",
"\n",
"# Optional: Check for errors in case tool use is flaky\n",
"def check_claude_output(tool_output):\n",
" \"\"\"Check for parse error or failure to call the tool\"\"\"\n",
"\n",
" # Error with parsing\n",
" if tool_output[\"parsing_error\"]:\n",
" # Report back output and parsing errors\n",
" print(\"Parsing error!\")\n",
" raw_output = str(tool_output[\"raw\"].content)\n",
" error = tool_output[\"parsing_error\"]\n",
" raise ValueError(\n",
" f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n",
" )\n",
"\n",
" # Tool was not invoked\n",
" elif not tool_output[\"parsed\"]:\n",
" print(\"Failed to invoke tool!\")\n",
" raise ValueError(\n",
" \"You did not use the provided tool! Be sure to invoke the tool to structure the output.\"\n",
" )\n",
" return tool_output\n",
"\n",
"\n",
"# Chain with output check\n",
"code_chain_claude_raw = (\n",
" code_gen_prompt_claude | structured_llm_claude | check_claude_output\n",
")\n",
"\n",
"\n",
"def insert_errors(inputs):\n",
" \"\"\"Insert errors for tool parsing in the messages\"\"\"\n",
"\n",
" # Get errors\n",
" error = inputs[\"error\"]\n",
" messages = inputs[\"messages\"]\n",
" messages += [\n",
" (\n",
" \"assistant\",\n",
" f\"Retry. You are required to fix the parsing errors: {error} \\n\\n You must invoke the provided tool.\",\n",
" )\n",
" ]\n",
" return {\n",
" \"messages\": messages,\n",
" \"context\": inputs[\"context\"],\n",
" }\n",
"\n",
"\n",
"# This will be run as a fallback chain\n",
"fallback_chain = insert_errors | code_chain_claude_raw\n",
"N = 3 # Max re-tries\n",
"code_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n",
" fallbacks=[fallback_chain] * N, exception_key=\"error\"\n",
")\n",
"\n",
"\n",
"def parse_output(solution):\n",
" \"\"\"When we add 'include_raw=True' to structured output,\n",
" it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n",
"\n",
" return solution[\"parsed\"]\n",
"\n",
"\n",
"# Optional: With re-try to correct for failure to invoke tool\n",
"code_gen_chain = code_gen_chain_re_try | parse_output\n",
"\n",
"# No re-try\n",
"code_gen_chain = code_gen_prompt_claude | structured_llm_claude | parse_output"
]
"source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n### Anthropic\n\n# Prompt to enforce tool use\ncode_gen_prompt_claude = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"<instructions> You are a coding assistant with expertise in LCEL, LangChain expression language. \\n \n Here is the LCEL documentation: \\n ------- \\n {context} \\n ------- \\n Answer the user question based on the \\n \n above provided documentation. Ensure any code you provide can be executed with all required imports and variables \\n\n defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block. \\n\n Invoke the code tool to structure the output correctly. </instructions> \\n Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\n# LLM\n# expt_llm = \"claude-3-haiku-20240307\"\nexpt_llm = \"claude-3-opus-20240229\"\nllm = ChatAnthropic(\n model=expt_llm,\n default_headers={\"anthropic-beta\": \"tools-2024-04-04\"},\n)\n\nstructured_llm_claude = llm.with_structured_output(code, include_raw=True)\n\n\n# Optional: Check for errors in case tool use is flaky\ndef check_claude_output(tool_output):\n \"\"\"Check for parse error or failure to call the tool\"\"\"\n\n # Error with parsing\n if tool_output[\"parsing_error\"]:\n # Report back output and parsing errors\n print(\"Parsing error!\")\n raw_output = str(tool_output[\"raw\"].content)\n error = tool_output[\"parsing_error\"]\n raise ValueError(\n f\"Error parsing your output! Be sure to invoke the tool. Output: {raw_output}. \\n Parse error: {error}\"\n )\n\n # Tool was not invoked\n elif not tool_output[\"parsed\"]:\n print(\"Failed to invoke tool!\")\n raise ValueError(\n \"You did not use the provided tool! Be sure to invoke the tool to structure the output.\"\n )\n return tool_output\n\n\n# Chain with output check\ncode_chain_claude_raw = (\n code_gen_prompt_claude | structured_llm_claude | check_claude_output\n)\n\n\ndef insert_errors(inputs):\n \"\"\"Insert errors for tool parsing in the messages\"\"\"\n\n # Get errors\n error = inputs[\"error\"]\n messages = inputs[\"messages\"]\n messages += [\n (\n \"assistant\",\n f\"Retry. You are required to fix the parsing errors: {error} \\n\\n You must invoke the provided tool.\",\n )\n ]\n return {\n \"messages\": messages,\n \"context\": inputs[\"context\"],\n }\n\n\n# This will be run as a fallback chain\nfallback_chain = insert_errors | code_chain_claude_raw\nN = 3 # Max re-tries\ncode_gen_chain_re_try = code_chain_claude_raw.with_fallbacks(\n fallbacks=[fallback_chain] * N, exception_key=\"error\"\n)\n\n\ndef parse_output(solution):\n \"\"\"When we add 'include_raw=True' to structured output,\n it will return a dict w 'raw', 'parsed', 'parsing_error'.\"\"\"\n\n return solution[\"parsed\"]\n\n\n# Optional: With re-try to correct for failure to invoke tool\ncode_gen_chain = code_gen_chain_re_try | parse_output\n\n# No re-try\ncode_gen_chain = code_gen_prompt_claude | structured_llm_claude | parse_output"]
},
{
"cell_type": "code",
@@ -260,14 +92,7 @@
"scrolled": true
},
"outputs": [],
"source": [
"# Test\n",
"question = \"How do I build a RAG chain in LCEL?\"\n",
"solution = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n",
")\n",
"solution"
]
"source": ["# Test\nquestion = \"How do I build a RAG chain in LCEL?\"\nsolution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": [(\"user\", question)]}\n)\nsolution"]
},
{
"cell_type": "markdown",
@@ -285,26 +110,7 @@
"id": "c185f1a2-e943-4bed-b833-4243c9c64092",
"metadata": {},
"outputs": [],
"source": [
"from typing import List, TypedDict\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" error : Binary flag for control flow to indicate whether test error was tripped\n",
" messages : With user question, error messages, reasoning\n",
" generation : Code solution\n",
" iterations : Number of tries\n",
" \"\"\"\n",
"\n",
" error: str\n",
" messages: List\n",
" generation: str\n",
" iterations: int"
]
"source": ["from typing import List, TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n error : Binary flag for control flow to indicate whether test error was tripped\n messages : With user question, error messages, reasoning\n generation : Code solution\n iterations : Number of tries\n \"\"\"\n\n error: str\n messages: List\n generation: str\n iterations: int"]
},
{
"cell_type": "markdown",
@@ -322,177 +128,7 @@
"id": "b70e8301-63ae-4f7e-ad8f-c9a052fe3566",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"### Parameter\n",
"\n",
"# Max tries\n",
"max_iterations = 3\n",
"# Reflect\n",
"# flag = 'reflect'\n",
"flag = \"do not reflect\"\n",
"\n",
"### Nodes\n",
"\n",
"\n",
"def generate(state: GraphState):\n",
" \"\"\"\n",
" Generate a code solution\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation\n",
" \"\"\"\n",
"\n",
" print(\"---GENERATING CODE SOLUTION---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" iterations = state[\"iterations\"]\n",
" error = state[\"error\"]\n",
"\n",
" # We have been routed back to generation with an error\n",
" if error == \"yes\":\n",
" messages += [\n",
" (\n",
" \"user\",\n",
" \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n",
" )\n",
" ]\n",
"\n",
" # Solution\n",
" code_solution = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": messages}\n",
" )\n",
" messages += [\n",
" (\n",
" \"assistant\",\n",
" f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n",
" )\n",
" ]\n",
"\n",
" # Increment\n",
" iterations = iterations + 1\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n",
"\n",
"\n",
"def code_check(state: GraphState):\n",
" \"\"\"\n",
" Check code\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, error\n",
" \"\"\"\n",
"\n",
" print(\"---CHECKING CODE---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" code_solution = state[\"generation\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" # Get solution components\n",
" imports = code_solution.imports\n",
" code = code_solution.code\n",
"\n",
" # Check imports\n",
" try:\n",
" exec(imports)\n",
" except Exception as e:\n",
" print(\"---CODE IMPORT CHECK: FAILED---\")\n",
" error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n",
" messages += error_message\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # Check execution\n",
" try:\n",
" exec(imports + \"\\n\" + code)\n",
" except Exception as e:\n",
" print(\"---CODE BLOCK CHECK: FAILED---\")\n",
" error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n",
" messages += error_message\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # No errors\n",
" print(\"---NO CODE TEST FAILURES---\")\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"no\",\n",
" }\n",
"\n",
"\n",
"def reflect(state: GraphState):\n",
" \"\"\"\n",
" Reflect on errors\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation\n",
" \"\"\"\n",
"\n",
" print(\"---GENERATING CODE SOLUTION---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" iterations = state[\"iterations\"]\n",
" code_solution = state[\"generation\"]\n",
"\n",
" # Prompt reflection\n",
"\n",
" # Add reflection\n",
" reflections = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": messages}\n",
" )\n",
" messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_finish(state: GraphState):\n",
" \"\"\"\n",
" Determines whether to finish.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Next node to call\n",
" \"\"\"\n",
" error = state[\"error\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" if error == \"no\" or iterations == max_iterations:\n",
" print(\"---DECISION: FINISH---\")\n",
" return \"end\"\n",
" else:\n",
" print(\"---DECISION: RE-TRY SOLUTION---\")\n",
" if flag == \"reflect\":\n",
" return \"reflect\"\n",
" else:\n",
" return \"generate\""
]
"source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n### Parameter\n\n# Max tries\nmax_iterations = 3\n# Reflect\n# flag = 'reflect'\nflag = \"do not reflect\"\n\n### Nodes\n\n\ndef generate(state: GraphState):\n \"\"\"\n Generate a code solution\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n error = state[\"error\"]\n\n # We have been routed back to generation with an error\n if error == \"yes\":\n messages += [\n (\n \"user\",\n \"Now, try again. Invoke the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n\n # Solution\n code_solution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": messages}\n )\n messages += [\n (\n \"assistant\",\n f\"{code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n )\n ]\n\n # Increment\n iterations = iterations + 1\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\ndef code_check(state: GraphState):\n \"\"\"\n Check code\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, error\n \"\"\"\n\n print(\"---CHECKING CODE---\")\n\n # State\n messages = state[\"messages\"]\n code_solution = state[\"generation\"]\n iterations = state[\"iterations\"]\n\n # Get solution components\n imports = code_solution.imports\n code = code_solution.code\n\n # Check imports\n try:\n exec(imports)\n except Exception as e:\n print(\"---CODE IMPORT CHECK: FAILED---\")\n error_message = [(\"user\", f\"Your solution failed the import test: {e}\")]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # Check execution\n try:\n exec(imports + \"\\n\" + code)\n except Exception as e:\n print(\"---CODE BLOCK CHECK: FAILED---\")\n error_message = [(\"user\", f\"Your solution failed the code execution test: {e}\")]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # No errors\n print(\"---NO CODE TEST FAILURES---\")\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"no\",\n }\n\n\ndef reflect(state: GraphState):\n \"\"\"\n Reflect on errors\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n code_solution = state[\"generation\"]\n\n # Prompt reflection\n\n # Add reflection\n reflections = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": messages}\n )\n messages += [(\"assistant\", f\"Here are reflections on the error: {reflections}\")]\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\n### Edges\n\n\ndef decide_to_finish(state: GraphState):\n \"\"\"\n Determines whether to finish.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n error = state[\"error\"]\n iterations = state[\"iterations\"]\n\n if error == \"no\" or iterations == max_iterations:\n print(\"---DECISION: FINISH---\")\n return \"end\"\n else:\n print(\"---DECISION: RE-TRY SOLUTION---\")\n if flag == \"reflect\":\n return \"reflect\"\n else:\n return \"generate\""]
},
{
"cell_type": "code",
@@ -500,31 +136,7 @@
"id": "f66b4e00-4731-42c8-bc38-72dd0ff7c92c",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"generate\", generate) # generation solution\n",
"workflow.add_node(\"check_code\", code_check) # check code\n",
"workflow.add_node(\"reflect\", reflect) # reflect\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"generate\")\n",
"workflow.add_edge(\"generate\", \"check_code\")\n",
"workflow.add_conditional_edges(\n",
" \"check_code\",\n",
" decide_to_finish,\n",
" {\n",
" \"end\": END,\n",
" \"reflect\": \"reflect\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"reflect\", \"generate\")\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"generate\", generate) # generation solution\nworkflow.add_node(\"check_code\", code_check) # check code\nworkflow.add_node(\"reflect\", reflect) # reflect\n\n# Build graph\nworkflow.add_edge(START, \"generate\")\nworkflow.add_edge(\"generate\", \"check_code\")\nworkflow.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"reflect\": \"reflect\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"reflect\", \"generate\")\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -532,10 +144,7 @@
"id": "9bcaafe4-ddcf-4fab-8620-2d9b6c508f98",
"metadata": {},
"outputs": [],
"source": [
"question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\n",
"app.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})"
]
"source": ["question = \"How can I directly pass a string to a runnable and use it to construct the input needed for my prompt?\"\napp.invoke({\"messages\": [(\"user\", question)], \"iterations\": 0})"]
},
{
"cell_type": "markdown",
@@ -563,11 +172,7 @@
"id": "678e8954-56b5-4cc6-be26-f7f2a060b242",
"metadata": {},
"outputs": [],
"source": [
"import langsmith\n",
"\n",
"client = langsmith.Client()"
]
"source": ["import langsmith\n\nclient = langsmith.Client()"]
},
{
"cell_type": "code",
@@ -575,13 +180,7 @@
"id": "ef7cf662-7a6f-4dee-965c-6309d4045feb",
"metadata": {},
"outputs": [],
"source": [
"# Clone the dataset to your tenant to use it\n",
"public_dataset = (\n",
" \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n",
")\n",
"client.clone_public_dataset(public_dataset)"
]
"source": ["# Clone the dataset to your tenant to use it\npublic_dataset = (\n \"https://smith.langchain.com/public/326674a6-62bd-462d-88ae-eea49d503f9d/d\"\n)\nclient.clone_public_dataset(public_dataset)"]
},
{
"cell_type": "markdown",
@@ -597,28 +196,7 @@
"id": "455a34ea-52cb-4ae5-9f4a-7e4a08cd0c09",
"metadata": {},
"outputs": [],
"source": [
"from langsmith.schemas import Example, Run\n",
"\n",
"\n",
"def check_import(run: Run, example: Example) -> dict:\n",
" imports = run.outputs.get(\"imports\")\n",
" try:\n",
" exec(imports)\n",
" return {\"key\": \"import_check\", \"score\": 1}\n",
" except Exception:\n",
" return {\"key\": \"import_check\", \"score\": 0}\n",
"\n",
"\n",
"def check_execution(run: Run, example: Example) -> dict:\n",
" imports = run.outputs.get(\"imports\")\n",
" code = run.outputs.get(\"code\")\n",
" try:\n",
" exec(imports + \"\\n\" + code)\n",
" return {\"key\": \"code_execution_check\", \"score\": 1}\n",
" except Exception:\n",
" return {\"key\": \"code_execution_check\", \"score\": 0}"
]
"source": ["from langsmith.schemas import Example, Run\n\n\ndef check_import(run: Run, example: Example) -> dict:\n imports = run.outputs.get(\"imports\")\n try:\n exec(imports)\n return {\"key\": \"import_check\", \"score\": 1}\n except Exception:\n return {\"key\": \"import_check\", \"score\": 0}\n\n\ndef check_execution(run: Run, example: Example) -> dict:\n imports = run.outputs.get(\"imports\")\n code = run.outputs.get(\"code\")\n try:\n exec(imports + \"\\n\" + code)\n return {\"key\": \"code_execution_check\", \"score\": 1}\n except Exception:\n return {\"key\": \"code_execution_check\", \"score\": 0}"]
},
{
"cell_type": "markdown",
@@ -634,22 +212,7 @@
"id": "c8fa6bcb-b245-4422-b79a-582cd8a7d7ea",
"metadata": {},
"outputs": [],
"source": [
"def predict_base_case(example: dict):\n",
" \"\"\"Context stuffing\"\"\"\n",
" solution = code_gen_chain.invoke(\n",
" {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n",
" )\n",
" solution_structured = code_gen_chain.invoke([(\"code\", solution)])\n",
" return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n",
"\n",
"\n",
"def predict_langgraph(example: dict):\n",
" \"\"\"LangGraph\"\"\"\n",
" graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n",
" solution = graph[\"generation\"]\n",
" return {\"imports\": solution.imports, \"code\": solution.code}"
]
"source": ["def predict_base_case(example: dict):\n \"\"\"Context stuffing\"\"\"\n solution = code_gen_chain.invoke(\n {\"context\": concatenated_content, \"messages\": [(\"user\", example[\"question\"])]}\n )\n solution_structured = code_gen_chain.invoke([(\"code\", solution)])\n return {\"imports\": solution_structured.imports, \"code\": solution_structured.code}\n\n\ndef predict_langgraph(example: dict):\n \"\"\"LangGraph\"\"\"\n graph = app.invoke({\"messages\": [(\"user\", example[\"question\"])], \"iterations\": 0})\n solution = graph[\"generation\"]\n return {\"imports\": solution.imports, \"code\": solution.code}"]
},
{
"cell_type": "code",
@@ -657,15 +220,7 @@
"id": "d9c57468-97f6-47d6-a5e9-c09b53bfdd83",
"metadata": {},
"outputs": [],
"source": [
"from langsmith.evaluation import evaluate\n",
"\n",
"# Evaluator\n",
"code_evalulator = [check_import, check_execution]\n",
"\n",
"# Dataset\n",
"dataset_name = \"test-LCEL-code-gen\""
]
"source": ["from langsmith.evaluation import evaluate\n\n# Evaluator\ncode_evalulator = [check_import, check_execution]\n\n# Dataset\ndataset_name = \"test-LCEL-code-gen\""]
},
{
"cell_type": "code",
@@ -673,19 +228,7 @@
"id": "2dacccf0-d73f-4017-aaf0-9806ffe5bd2c",
"metadata": {},
"outputs": [],
"source": [
"# Run base case\n",
"experiment_results_ = evaluate(\n",
" predict_base_case,\n",
" data=dataset_name,\n",
" evaluators=code_evalulator,\n",
" experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n",
" max_concurrency=2,\n",
" metadata={\n",
" \"llm\": expt_llm,\n",
" },\n",
")"
]
"source": ["# Run base case\nexperiment_results_ = evaluate(\n predict_base_case,\n data=dataset_name,\n evaluators=code_evalulator,\n experiment_prefix=f\"test-without-langgraph-{expt_llm}\",\n max_concurrency=2,\n metadata={\n \"llm\": expt_llm,\n },\n)"]
},
{
"cell_type": "code",
@@ -693,20 +236,7 @@
"id": "71d90f9e-9dad-410c-a709-093d275029ae",
"metadata": {},
"outputs": [],
"source": [
"# Run with langgraph\n",
"experiment_results = evaluate(\n",
" predict_langgraph,\n",
" data=dataset_name,\n",
" evaluators=code_evalulator,\n",
" experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n",
" max_concurrency=2,\n",
" metadata={\n",
" \"llm\": expt_llm,\n",
" \"feedback\": flag,\n",
" },\n",
")"
]
"source": ["# Run with langgraph\nexperiment_results = evaluate(\n predict_langgraph,\n data=dataset_name,\n evaluators=code_evalulator,\n experiment_prefix=f\"test-with-langgraph-{expt_llm}-{flag}\",\n max_concurrency=2,\n metadata={\n \"llm\": expt_llm,\n \"feedback\": flag,\n },\n)"]
},
{
"cell_type": "markdown",
@@ -728,7 +258,7 @@
"id": "a42333c3-c098-4576-ae2a-0258de64ece2",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -33,9 +33,7 @@
"id": "e501686f-323f-4b87-8f9c-8ba89133078b",
"metadata": {},
"outputs": [],
"source": [
"! pip install -U langchain_community langchain-mistralai langchain langgraph"
]
"source": ["! pip install -U langchain_community langchain-mistralai langchain langgraph"]
},
{
"cell_type": "markdown",
@@ -53,12 +51,7 @@
"id": "982e4609-86e4-4934-828f-e03d89c20393",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\n",
"mistral_api_key = os.getenv(\"MISTRAL_API_KEY\") # Ensure this is set"
]
"source": ["import os\n\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"true\"\nmistral_api_key = os.getenv(\"MISTRAL_API_KEY\") # Ensure this is set"]
},
{
"cell_type": "markdown",
@@ -76,12 +69,7 @@
"id": "37b172d2-3a9d-49a8-898c-22ed0cb45c88",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Mistral-code-gen-testing\""
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Mistral-code-gen-testing\""]
},
{
"cell_type": "markdown",
@@ -99,42 +87,7 @@
"id": "a188c8ca-c053-4e6d-b7af-38a3b6b371c7",
"metadata": {},
"outputs": [],
"source": [
"# Select LLM\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_mistralai import ChatMistralAI\n",
"\n",
"mistral_model = \"mistral-large-latest\"\n",
"llm = ChatMistralAI(model=mistral_model, temperature=0)\n",
"\n",
"# Prompt\n",
"code_gen_prompt_claude = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are a coding assistant. Ensure any code you provide can be executed with all required imports and variables \\n\n",
" defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block.\n",
" \\n Here is the user question:\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"# Data model\n",
"class code(BaseModel):\n",
" \"\"\"Code output\"\"\"\n",
"\n",
" prefix: str = Field(description=\"Description of the problem and approach\")\n",
" imports: str = Field(description=\"Code block import statements\")\n",
" code: str = Field(description=\"Code block not including import statements\")\n",
" description = \"Schema for code solutions to questions about LCEL.\"\n",
"\n",
"\n",
"# LLM\n",
"code_gen_chain = llm.with_structured_output(code, include_raw=False)"
]
"source": ["# Select LLM\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_mistralai import ChatMistralAI\n\nmistral_model = \"mistral-large-latest\"\nllm = ChatMistralAI(model=mistral_model, temperature=0)\n\n# Prompt\ncode_gen_prompt_claude = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a coding assistant. Ensure any code you provide can be executed with all required imports and variables \\n\n defined. Structure your answer: 1) a prefix describing the code solution, 2) the imports, 3) the functioning code block.\n \\n Here is the user question:\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\n\n# Data model\nclass code(BaseModel):\n \"\"\"Code output\"\"\"\n\n prefix: str = Field(description=\"Description of the problem and approach\")\n imports: str = Field(description=\"Code block import statements\")\n code: str = Field(description=\"Code block not including import statements\")\n description = \"Schema for code solutions to questions about LCEL.\"\n\n\n# LLM\ncode_gen_chain = llm.with_structured_output(code, include_raw=False)"]
},
{
"cell_type": "code",
@@ -142,10 +95,7 @@
"id": "9fc0290d-5a04-4514-8664-91f9dbf2da7b",
"metadata": {},
"outputs": [],
"source": [
"question = \"Write a function for fibonacci.\"\n",
"messages = [(\"user\", question)]"
]
"source": ["question = \"Write a function for fibonacci.\"\nmessages = [(\"user\", question)]"]
},
{
"cell_type": "code",
@@ -164,11 +114,7 @@
"output_type": "execute_result"
}
],
"source": [
"# Test\n",
"result = code_gen_chain.invoke(messages)\n",
"result"
]
"source": ["# Test\nresult = code_gen_chain.invoke(messages)\nresult"]
},
{
"cell_type": "markdown",
@@ -184,28 +130,7 @@
"id": "183d77b8-f180-4815-b39f-8ef507ec0534",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated, TypedDict\n",
"\n",
"from langgraph.graph.message import AnyMessage, add_messages\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" error : Binary flag for control flow to indicate whether test error was tripped\n",
" messages : With user question, error messages, reasoning\n",
" generation : Code solution\n",
" iterations : Number of tries\n",
" \"\"\"\n",
"\n",
" error: str\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
" generation: str\n",
" iterations: int"
]
"source": ["from typing import Annotated, TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n error : Binary flag for control flow to indicate whether test error was tripped\n messages : With user question, error messages, reasoning\n generation : Code solution\n iterations : Number of tries\n \"\"\"\n\n error: str\n messages: Annotated[list[AnyMessage], add_messages]\n generation: str\n iterations: int"]
},
{
"cell_type": "markdown",
@@ -221,163 +146,7 @@
"id": "14bc89d1-3ca6-4847-a048-1803e0e4600e",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"### Parameters\n",
"max_iterations = 3\n",
"\n",
"\n",
"### Nodes\n",
"def generate(state: GraphState):\n",
" \"\"\"\n",
" Generate a code solution\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation\n",
" \"\"\"\n",
"\n",
" print(\"---GENERATING CODE SOLUTION---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" # Solution\n",
" code_solution = code_gen_chain.invoke(messages)\n",
" messages += [\n",
" (\n",
" \"assistant\",\n",
" f\"Here is my attempt to solve the problem: {code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n",
" )\n",
" ]\n",
"\n",
" # Increment\n",
" iterations = iterations + 1\n",
" return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n",
"\n",
"\n",
"def code_check(state: GraphState):\n",
" \"\"\"\n",
" Check code\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, error\n",
" \"\"\"\n",
"\n",
" print(\"---CHECKING CODE---\")\n",
"\n",
" # State\n",
" messages = state[\"messages\"]\n",
" code_solution = state[\"generation\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" # Get solution components\n",
" imports = code_solution.imports\n",
" code = code_solution.code\n",
"\n",
" # Check imports\n",
" try:\n",
" exec(imports)\n",
" except Exception as e:\n",
" print(\"---CODE IMPORT CHECK: FAILED---\")\n",
" error_message = [\n",
" (\n",
" \"user\",\n",
" f\"Your solution failed the import test. Here is the error: {e}. Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n",
" )\n",
" ]\n",
" messages += error_message\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # Check execution\n",
" try:\n",
" combined_code = f\"{imports}\\n{code}\"\n",
" print(f\"CODE TO TEST: {combined_code}\")\n",
" # Use a shared scope for exec\n",
" global_scope = {}\n",
" exec(combined_code, global_scope)\n",
" except Exception as e:\n",
" print(\"---CODE BLOCK CHECK: FAILED---\")\n",
" error_message = [\n",
" (\n",
" \"user\",\n",
" f\"Your solution failed the code execution test: {e}) Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n",
" )\n",
" ]\n",
" messages += error_message\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"yes\",\n",
" }\n",
"\n",
" # No errors\n",
" print(\"---NO CODE TEST FAILURES---\")\n",
" return {\n",
" \"generation\": code_solution,\n",
" \"messages\": messages,\n",
" \"iterations\": iterations,\n",
" \"error\": \"no\",\n",
" }\n",
"\n",
"\n",
"### Conditional edges\n",
"\n",
"\n",
"def decide_to_finish(state: GraphState):\n",
" \"\"\"\n",
" Determines whether to finish.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Next node to call\n",
" \"\"\"\n",
" error = state[\"error\"]\n",
" iterations = state[\"iterations\"]\n",
"\n",
" if error == \"no\" or iterations == max_iterations:\n",
" print(\"---DECISION: FINISH---\")\n",
" return \"end\"\n",
" else:\n",
" print(\"---DECISION: RE-TRY SOLUTION---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"### Utilities\n",
"\n",
"\n",
"def _print_event(event: dict, _printed: set, max_length=1500):\n",
" current_state = event.get(\"dialog_state\")\n",
" if current_state:\n",
" print(\"Currently in: \", current_state[-1])\n",
" message = event.get(\"messages\")\n",
" if message:\n",
" if isinstance(message, list):\n",
" message = message[-1]\n",
" if message.id not in _printed:\n",
" msg_repr = message.pretty_repr(html=True)\n",
" if len(msg_repr) > max_length:\n",
" msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n",
" print(msg_repr)\n",
" _printed.add(message.id)"
]
"source": ["import uuid\n\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n### Parameters\nmax_iterations = 3\n\n\n### Nodes\ndef generate(state: GraphState):\n \"\"\"\n Generate a code solution\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation\n \"\"\"\n\n print(\"---GENERATING CODE SOLUTION---\")\n\n # State\n messages = state[\"messages\"]\n iterations = state[\"iterations\"]\n\n # Solution\n code_solution = code_gen_chain.invoke(messages)\n messages += [\n (\n \"assistant\",\n f\"Here is my attempt to solve the problem: {code_solution.prefix} \\n Imports: {code_solution.imports} \\n Code: {code_solution.code}\",\n )\n ]\n\n # Increment\n iterations = iterations + 1\n return {\"generation\": code_solution, \"messages\": messages, \"iterations\": iterations}\n\n\ndef code_check(state: GraphState):\n \"\"\"\n Check code\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, error\n \"\"\"\n\n print(\"---CHECKING CODE---\")\n\n # State\n messages = state[\"messages\"]\n code_solution = state[\"generation\"]\n iterations = state[\"iterations\"]\n\n # Get solution components\n imports = code_solution.imports\n code = code_solution.code\n\n # Check imports\n try:\n exec(imports)\n except Exception as e:\n print(\"---CODE IMPORT CHECK: FAILED---\")\n error_message = [\n (\n \"user\",\n f\"Your solution failed the import test. Here is the error: {e}. Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # Check execution\n try:\n combined_code = f\"{imports}\\n{code}\"\n print(f\"CODE TO TEST: {combined_code}\")\n # Use a shared scope for exec\n global_scope = {}\n exec(combined_code, global_scope)\n except Exception as e:\n print(\"---CODE BLOCK CHECK: FAILED---\")\n error_message = [\n (\n \"user\",\n f\"Your solution failed the code execution test: {e}) Reflect on this error and your prior attempt to solve the problem. (1) State what you think went wrong with the prior solution and (2) try to solve this problem again. Return the FULL SOLUTION. Use the code tool to structure the output with a prefix, imports, and code block:\",\n )\n ]\n messages += error_message\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"yes\",\n }\n\n # No errors\n print(\"---NO CODE TEST FAILURES---\")\n return {\n \"generation\": code_solution,\n \"messages\": messages,\n \"iterations\": iterations,\n \"error\": \"no\",\n }\n\n\n### Conditional edges\n\n\ndef decide_to_finish(state: GraphState):\n \"\"\"\n Determines whether to finish.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n error = state[\"error\"]\n iterations = state[\"iterations\"]\n\n if error == \"no\" or iterations == max_iterations:\n print(\"---DECISION: FINISH---\")\n return \"end\"\n else:\n print(\"---DECISION: RE-TRY SOLUTION---\")\n return \"generate\"\n\n\n### Utilities\n\n\ndef _print_event(event: dict, _printed: set, max_length=1500):\n current_state = event.get(\"dialog_state\")\n if current_state:\n print(\"Currently in: \", current_state[-1])\n message = event.get(\"messages\")\n if message:\n if isinstance(message, list):\n message = message[-1]\n if message.id not in _printed:\n msg_repr = message.pretty_repr(html=True)\n if len(msg_repr) > max_length:\n msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n print(msg_repr)\n _printed.add(message.id)"]
},
{
"cell_type": "code",
@@ -385,31 +154,7 @@
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"builder = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"builder.add_node(\"generate\", generate) # generation solution\n",
"builder.add_node(\"check_code\", code_check) # check code\n",
"\n",
"# Build graph\n",
"builder.set_entry_point(\"generate\")\n",
"builder.add_edge(\"generate\", \"check_code\")\n",
"builder.add_conditional_edges(\n",
" \"check_code\",\n",
" decide_to_finish,\n",
" {\n",
" \"end\": END,\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
"graph = builder.compile(checkpointer=memory)"
]
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
@@ -428,15 +173,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "code",
@@ -444,23 +181,7 @@
"id": "242aa2f0-2c31-462f-a958-ff9ae0cf7c62",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"Write a Python program that prints 'Hello, World!' to the console.\"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"Write a Python program that prints 'Hello, World!' to the console.\"\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -478,31 +199,7 @@
"id": "390b2768-f395-4aea-8b0e-9d36212a31ac",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"Create a Python program that checks if a given string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward (ignoring spaces, punctuation, and capitalization).\n",
"\n",
"Requirements:\n",
"The program should define a function is_palindrome(s) that takes a string s as input.\n",
"The function should return True if the string is a palindrome and False otherwise.\n",
"Ignore spaces, punctuation, and case differences when checking for palindromes.\n",
"\n",
"Give an example of it working on an example input word.\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Create a Python program that checks if a given string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward (ignoring spaces, punctuation, and capitalization).\n\nRequirements:\nThe program should define a function is_palindrome(s) that takes a string s as input.\nThe function should return True if the string is a palindrome and False otherwise.\nIgnore spaces, punctuation, and case differences when checking for palindromes.\n\nGive an example of it working on an example input word.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -520,26 +217,7 @@
"id": "0a3f946b-e2f2-44d9-905b-09f36980cf9f",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"Write a program that prints the numbers from 1 to 100. \n",
"But for multiples of three, print \"Fizz\" instead of the number, and for the multiples of five, print \"Buzz\". \n",
"For numbers which are multiples of both three and five, print \"FizzBuzz\".\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Write a program that prints the numbers from 1 to 100. \nBut for multiples of three, print \"Fizz\" instead of the number, and for the multiples of five, print \"Buzz\". \nFor numbers which are multiples of both three and five, print \"FizzBuzz\".\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -557,37 +235,7 @@
"id": "2bb883df-540b-46ab-9415-fe27db68456f",
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"I want to vectorize a function\n",
"\n",
" frame = np.zeros((out_h, out_w, 3), dtype=np.uint8)\n",
" for i, val1 in enumerate(rows):\n",
" for j, val2 in enumerate(cols):\n",
" for j, val3 in enumerate(ch):\n",
" # Assuming you want to store the pair as tuples in the matrix\n",
" frame[i, j, k] = image[val1, val2, val3]\n",
"\n",
" out.write(np.array(frame))\n",
"\n",
"with a simple numpy function that does something like this what is it called. Show me a test case with this working.\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["import uuid\n\n_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"I want to vectorize a function\n\n frame = np.zeros((out_h, out_w, 3), dtype=np.uint8)\n for i, val1 in enumerate(rows):\n for j, val2 in enumerate(cols):\n for j, val3 in enumerate(ch):\n # Assuming you want to store the pair as tuples in the matrix\n frame[i, j, k] = image[val1, val2, val3]\n\n out.write(np.array(frame))\n\nwith a simple numpy function that does something like this what is it called. Show me a test case with this working.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -605,34 +253,7 @@
"id": "ee05da1f-c272-405d-8a7b-552cfc3106e1",
"metadata": {},
"outputs": [],
"source": [
"_printed = set()\n",
"thread_id = str(uuid.uuid4())\n",
"config = {\n",
" \"configurable\": {\n",
" # Checkpoints are accessed by thread_id\n",
" \"thread_id\": thread_id,\n",
" }\n",
"}\n",
"\n",
"question = \"\"\"Create a Python program that allows two players to play a game of Tic-Tac-Toe. The game should be played on a 3x3 grid. The program should:\n",
"\n",
"- Allow players to take turns to input their moves.\n",
"- Check for invalid moves (e.g., placing a marker on an already occupied space).\n",
"- Determine and announce the winner or if the game ends in a draw.\n",
"\n",
"Requirements:\n",
"- Use a 2D list to represent the Tic-Tac-Toe board.\n",
"- Use functions to modularize the code.\n",
"- Validate player input.\n",
"- Check for win conditions and draw conditions after each move.\"\"\"\n",
"\n",
"events = graph.stream(\n",
" {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n",
")\n",
"for event in events:\n",
" _print_event(event, _printed)"
]
"source": ["_printed = set()\nthread_id = str(uuid.uuid4())\nconfig = {\n \"configurable\": {\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\nquestion = \"\"\"Create a Python program that allows two players to play a game of Tic-Tac-Toe. The game should be played on a 3x3 grid. The program should:\n\n- Allow players to take turns to input their moves.\n- Check for invalid moves (e.g., placing a marker on an already occupied space).\n- Determine and announce the winner or if the game ends in a draw.\n\nRequirements:\n- Use a 2D list to represent the Tic-Tac-Toe board.\n- Use functions to modularize the code.\n- Validate player input.\n- Check for win conditions and draw conditions after each move.\"\"\"\n\nevents = graph.stream(\n {\"messages\": [(\"user\", question)], \"iterations\": 0}, config, stream_mode=\"values\"\n)\nfor event in events:\n _print_event(event, _printed)"]
},
{
"cell_type": "markdown",
@@ -650,7 +271,7 @@
"id": "814fc2a4-8e5b-4faa-8f52-3977226bd09a",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+9 -95
View File
@@ -28,35 +28,7 @@
"id": "816523d0-0b59-47cf-9f4c-4838024efe22",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"model = ChatAnthropic(model_name=\"claude-2.1\")\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]\n",
"\n",
"\n",
"def _call_model(state):\n",
" response = model.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
"workflow.set_entry_point(\"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
"source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.messages import BaseMessage, HumanMessage\n\nfrom langgraph.graph import END, StateGraph, START\n\nmodel = ChatAnthropic(model_name=\"claude-2.1\")\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n\n\ndef _call_model(state):\n response = model.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -76,9 +48,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"
]
"source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"]
},
{
"cell_type": "markdown",
@@ -98,31 +68,7 @@
"id": "c01f1e7c-8e8b-4e26-98f7-56ac225077b4",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"openai_model = ChatOpenAI()\n",
"\n",
"models = {\n",
" \"anthropic\": model,\n",
" \"openai\": openai_model,\n",
"}\n",
"\n",
"\n",
"def _call_model(state, config):\n",
" m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n",
" response = m.invoke(state[\"messages\"])\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
"workflow.set_entry_point(\"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
"source": ["from langchain_openai import ChatOpenAI\n\nopenai_model = ChatOpenAI()\n\nmodels = {\n \"anthropic\": model,\n \"openai\": openai_model,\n}\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n response = m.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"]
},
{
"cell_type": "markdown",
@@ -150,9 +96,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"
]
"source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"]
},
{
"cell_type": "markdown",
@@ -180,10 +124,7 @@
"output_type": "execute_result"
}
],
"source": [
"config = {\"configurable\": {\"model\": \"openai\"}}\n",
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"
]
"source": ["config = {\"configurable\": {\"model\": \"openai\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"]
},
{
"cell_type": "markdown",
@@ -199,29 +140,7 @@
"id": "f0393a43-9fbe-4056-972f-3e91ea329041",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import SystemMessage\n",
"\n",
"\n",
"def _call_model(state, config):\n",
" m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n",
" messages = state[\"messages\"]\n",
" if \"system_message\" in config[\"configurable\"]:\n",
" messages = [\n",
" SystemMessage(content=config[\"configurable\"][\"system_message\"])\n",
" ] + messages\n",
" response = m.invoke(messages)\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"model\", _call_model)\n",
"workflow.set_entry_point(\"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"\n",
"app = workflow.compile()"
]
"source": ["from langchain_core.messages import SystemMessage\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n messages = state[\"messages\"]\n if \"system_message\" in config[\"configurable\"]:\n messages = [\n SystemMessage(content=config[\"configurable\"][\"system_message\"])\n ] + messages\n response = m.invoke(messages)\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -241,9 +160,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"
]
"source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"]
},
{
"cell_type": "code",
@@ -263,10 +180,7 @@
"output_type": "execute_result"
}
],
"source": [
"config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\n",
"app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"
]
"source": ["config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"]
},
{
"cell_type": "code",
@@ -274,7 +188,7 @@
"id": "a5c5f7f4-4b0e-4cde-93a6-c1c6329b8591",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
+10 -93
View File
@@ -12,46 +12,21 @@
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain-openai"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-openai"]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"if not os.environ.get(\"OPENAI_API_KEY\"):\n",
" os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"
]
"source": ["import getpass\nimport os\n\nif not os.environ.get(\"OPENAI_API_KEY\"):\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.graph import END, MessageGraph\n",
"\n",
"model = ChatOpenAI(temperature=0)\n",
"\n",
"graph = MessageGraph()\n",
"\n",
"graph.add_node(\"oracle\", model)\n",
"graph.add_edge(\"oracle\", END)\n",
"\n",
"graph.set_entry_point(\"oracle\")\n",
"\n",
"runnable = graph.compile()"
]
"source": ["from langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, MessageGraph\n\nmodel = ChatOpenAI(temperature=0)\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model)\ngraph.add_edge(\"oracle\", END)\n\ngraph.add_edge(START, \"oracle\")\n\nrunnable = graph.compile()"]
},
{
"cell_type": "code",
@@ -69,15 +44,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "code",
@@ -96,54 +63,14 @@
"output_type": "execute_result"
}
],
"source": [
"runnable.invoke(HumanMessage(\"What is 1 + 1?\"))"
]
"source": ["runnable.invoke(HumanMessage(\"What is 1 + 1?\"))"]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.graph import END, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"\n",
"@tool\n",
"def multiply(first_number: int, second_number: int):\n",
" \"\"\"Multiplies two numbers together.\"\"\"\n",
" return first_number * second_number\n",
"\n",
"\n",
"model = ChatOpenAI(temperature=0)\n",
"model_with_tools = model.bind_tools(tools=[multiply])\n",
"\n",
"graph = MessageGraph()\n",
"\n",
"graph.add_node(\"oracle\", model_with_tools)\n",
"\n",
"tool_node = ToolNode([multiply])\n",
"graph.add_node(\"multiply\", tool_node)\n",
"graph.add_edge(START, \"oracle\")\n",
"graph.add_edge(\"multiply\", END)\n",
"\n",
"\n",
"def router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n",
" tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n",
" if len(tool_calls):\n",
" return \"multiply\"\n",
" else:\n",
" return END\n",
"\n",
"\n",
"graph.add_conditional_edges(\"oracle\", router)\n",
"runnable = graph.compile()"
]
"source": ["from typing import Literal\n\nfrom langchain_core.tools import tool\n\nfrom langgraph.graph import END, START\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef multiply(first_number: int, second_number: int):\n \"\"\"Multiplies two numbers together.\"\"\"\n return first_number * second_number\n\n\nmodel = ChatOpenAI(temperature=0)\nmodel_with_tools = model.bind_tools(tools=[multiply])\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model_with_tools)\n\ntool_node = ToolNode([multiply])\ngraph.add_node(\"multiply\", tool_node)\ngraph.add_edge(START, \"oracle\")\ngraph.add_edge(\"multiply\", END)\n\n\ndef router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n if len(tool_calls):\n return \"multiply\"\n else:\n return END\n\n\ngraph.add_conditional_edges(\"oracle\", router)\nrunnable = graph.compile()"]
},
{
"cell_type": "code",
@@ -161,13 +88,7 @@
"output_type": "display_data"
}
],
"source": [
"try:\n",
" display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["try:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "code",
@@ -187,9 +108,7 @@
"output_type": "execute_result"
}
],
"source": [
"runnable.invoke(HumanMessage(\"What is 123 * 456?\"))"
]
"source": ["runnable.invoke(HumanMessage(\"What is 123 * 456?\"))"]
},
{
"cell_type": "code",
@@ -208,16 +127,14 @@
"output_type": "execute_result"
}
],
"source": [
"runnable.invoke(HumanMessage(\"What is your name?\"))"
]
"source": ["runnable.invoke(HumanMessage(\"What is your name?\"))"]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+18 -201
View File
@@ -28,10 +28,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_community langchain_openai tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_community langchain_openai tavily-python"]
},
{
"cell_type": "markdown",
@@ -47,19 +44,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")\n",
"_set_env(\"TAVILY_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -75,10 +60,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -104,19 +86,7 @@
"id": "4a1b9990-3b11-4a51-bd51-76117afd38b9",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"\n",
"class SearchTool(BaseModel):\n",
" \"\"\"Look up things online, optionally returning directly\"\"\"\n",
"\n",
" query: str = Field(description=\"query to look up online\")\n",
" return_direct: bool = Field(\n",
" description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\",\n",
" default=False,\n",
" )"
]
"source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass SearchTool(BaseModel):\n \"\"\"Look up things online, optionally returning directly\"\"\"\n\n query: str = Field(description=\"query to look up online\")\n return_direct: bool = Field(\n description=\"Whether or the result of this should be returned directly to the user without you seeing what it is\",\n default=False,\n )"]
},
{
"cell_type": "code",
@@ -124,12 +94,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"search_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\n",
"tools = [search_tool]"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\nsearch_tool = TavilySearchResults(max_results=1, args_schema=SearchTool)\ntools = [search_tool]"]
},
{
"cell_type": "markdown",
@@ -147,11 +112,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"tool_executor = ToolExecutor(tools)"
]
"source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
},
{
"cell_type": "markdown",
@@ -175,11 +136,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "markdown",
@@ -197,9 +154,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -226,14 +181,7 @@
"id": "ea793afa-2eab-4901-910d-6eed90cd6564",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[list, operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict\n\n\nclass AgentState(TypedDict):\n messages: Annotated[list, operator.add]"]
},
{
"cell_type": "markdown",
@@ -268,11 +216,7 @@
"id": "03308b6b-de72-4cdc-b6c6-47e654df340e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation"
]
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation"]
},
{
"cell_type": "markdown",
@@ -290,22 +234,7 @@
"id": "55e088b1-f3c8-4798-9ca8-5b0be961b49a",
"metadata": {},
"outputs": [],
"source": [
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we check if it's suppose to return direct\n",
" else:\n",
" arguments = last_message.tool_calls[0][\"args\"]\n",
" if arguments.get(\"return_direct\", False):\n",
" return \"final\"\n",
" else:\n",
" return \"continue\""
]
"source": ["# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we check if it's suppose to return direct\n else:\n arguments = last_message.tool_calls[0][\"args\"]\n if arguments.get(\"return_direct\", False):\n return \"final\"\n else:\n return \"continue\""]
},
{
"cell_type": "code",
@@ -313,14 +242,7 @@
"id": "2b45da72-1afa-4cd7-9b7f-49a7c99cdb8a",
"metadata": {},
"outputs": [],
"source": [
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}"
]
"source": ["# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"]
},
{
"cell_type": "markdown",
@@ -338,33 +260,7 @@
"id": "dd876f5d-88d6-4f93-b1d0-f2f0b6f4d991",
"metadata": {},
"outputs": [],
"source": [
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" tool_name = tool_call[\"name\"]\n",
" arguments = tool_call[\"args\"]\n",
" if tool_name == \"tavily_search_results_json\":\n",
" if \"return_direct\" in arguments:\n",
" del arguments[\"return_direct\"]\n",
" action = ToolInvocation(\n",
" tool=tool_name,\n",
" tool_input=arguments,\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}"
]
"source": ["# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n tool_name = tool_call[\"name\"]\n arguments = tool_call[\"args\"]\n if tool_name == \"tavily_search_results_json\":\n if \"return_direct\" in arguments:\n del arguments[\"return_direct\"]\n action = ToolInvocation(\n tool=tool_name,\n tool_input=arguments,\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"]
},
{
"cell_type": "markdown",
@@ -386,56 +282,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"\n",
"# Note the \"action\" and \"final\" nodes are identical!\n",
"workflow.add_node(\"action\", call_tool)\n",
"workflow.add_node(\"final\", call_tool)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Final call\n",
" \"final\": \"final\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"workflow.add_edge(\"final\", END)\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\n\n# Note the \"action\" and \"final\" nodes are identical!\nworkflow.add_node(\"action\", call_tool)\nworkflow.add_node(\"final\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Final call\n \"final\": \"final\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\nworkflow.add_edge(\"final\", END)\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -454,11 +301,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -502,18 +345,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"for output in app.stream(inputs):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value)\n",
" print(\"\\n---\\n\")"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"]
},
{
"cell_type": "code",
@@ -563,22 +395,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"what is the weather in sf? return this result directly by setting return_direct = True\"\n",
" )\n",
" ]\n",
"}\n",
"for output in app.stream(inputs, stream_mode=\"values\"):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for message in output[\"messages\"]:\n",
" message.pretty_print()\n",
" print(\"\\n---\\n\")"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\n \"messages\": [\n HumanMessage(\n content=\"what is the weather in sf? return this result directly by setting return_direct = True\"\n )\n ]\n}\nfor output in app.stream(inputs, stream_mode=\"values\"):\n # stream() yields dictionaries with output keyed by node name\n for message in output[\"messages\"]:\n message.pretty_print()\n print(\"\\n---\\n\")"]
},
{
"cell_type": "code",
@@ -586,7 +403,7 @@
"id": "49ccc134-4abe-4982-8ecd-d70fc56a4d2d",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
+14 -201
View File
@@ -26,10 +26,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain langchain_openai tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai tavily-python"]
},
{
"cell_type": "markdown",
@@ -45,13 +42,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
"os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
]
"source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"]
},
{
"cell_type": "markdown",
@@ -67,10 +58,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -90,11 +78,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"tools = [TavilySearchResults(max_results=1)]"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"]
},
{
"cell_type": "markdown",
@@ -112,11 +96,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"tool_executor = ToolExecutor(tools)"
]
"source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
},
{
"cell_type": "markdown",
@@ -140,13 +120,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"# We will set streaming=True so that we can stream tokens\n",
"# See the streaming section for more information on this.\n",
"model = ChatOpenAI(temperature=0, streaming=True)"
]
"source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"]
},
{
"cell_type": "markdown",
@@ -164,9 +138,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -192,16 +164,7 @@
"id": "ea793afa-2eab-4901-910d-6eed90cd6564",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"]
},
{
"cell_type": "markdown",
@@ -236,66 +199,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation for each tool call\n",
" tool_invocations = []\n",
" for tool_call in last_message.tool_calls:\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" tool_invocations.append(action)\n",
"\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n",
" # We use the response to create tool messages\n",
" tool_messages = [\n",
" ToolMessage(\n",
" content=str(response),\n",
" name=tc[\"name\"],\n",
" tool_call_id=tc[\"id\"],\n",
" )\n",
" for tc, response in zip(last_message.tool_calls, responses)\n",
" ]\n",
"\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": tool_messages}"
]
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"]
},
{
"cell_type": "markdown",
@@ -313,30 +217,7 @@
"id": "1bfd2b22-292a-4f4d-91a0-46bb704f5e38",
"metadata": {},
"outputs": [],
"source": [
"# This is the new first - the first call of the model we want to explicitly hard-code some action\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"def first_model(state):\n",
" human_input = state[\"messages\"][-1].content\n",
" return {\n",
" \"messages\": [\n",
" AIMessage(\n",
" content=\"\",\n",
" tool_calls=[\n",
" {\n",
" \"name\": \"tavily_search_results_json\",\n",
" \"args\": {\n",
" \"query\": human_input,\n",
" },\n",
" \"id\": \"tool_abcd123\",\n",
" }\n",
" ],\n",
" )\n",
" ]\n",
" }"
]
"source": ["# This is the new first - the first call of the model we want to explicitly hard-code some action\nfrom langchain_core.messages import AIMessage\n\n\ndef first_model(state):\n human_input = state[\"messages\"][-1].content\n return {\n \"messages\": [\n AIMessage(\n content=\"\",\n tool_calls=[\n {\n \"name\": \"tavily_search_results_json\",\n \"args\": {\n \"query\": human_input,\n },\n \"id\": \"tool_abcd123\",\n }\n ],\n )\n ]\n }"]
},
{
"cell_type": "markdown",
@@ -358,56 +239,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the new entrypoint\n",
"workflow.add_node(\"first_agent\", first_model)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"first_agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# After we call the first agent, we know we want to go to action\n",
"workflow.add_edge(\"first_agent\", \"action\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the new entrypoint\nworkflow.add_node(\"first_agent\", first_model)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"first_agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After we call the first agent, we know we want to go to action\nworkflow.add_edge(\"first_agent\", \"action\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -426,15 +258,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "markdown",
@@ -478,18 +302,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"for output in app.stream(inputs):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value)\n",
" print(\"\\n---\\n\")"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"]
},
{
"cell_type": "code",
@@ -497,7 +310,7 @@
"id": "08ae8246-11d5-40e1-8567-361e5bef8917",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+24 -329
View File
@@ -39,10 +39,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
},
{
"cell_type": "markdown",
@@ -58,18 +55,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -85,10 +71,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -106,22 +89,7 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# `add_messages`` essentially does this\n",
"# (with more robust handling)\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
},
{
"cell_type": "markdown",
@@ -141,22 +109,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -174,11 +127,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"tool_executor = ToolExecutor(tools)"
]
"source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
},
{
"cell_type": "markdown",
@@ -199,11 +148,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "markdown",
@@ -221,9 +166,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -258,53 +201,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}"
]
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"]
},
{
"cell_type": "markdown",
@@ -322,45 +219,7 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
},
{
"cell_type": "markdown",
@@ -378,11 +237,7 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
]
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
},
{
"cell_type": "markdown",
@@ -400,12 +255,7 @@
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
"metadata": {},
"outputs": [],
"source": [
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
},
{
"cell_type": "markdown",
@@ -432,11 +282,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -467,14 +313,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"inputs = [HumanMessage(content=\"hi! I'm bob\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"2\"}}\ninputs = [HumanMessage(content=\"hi! I'm bob\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -495,11 +334,7 @@
]
}
],
"source": [
"inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["inputs = [HumanMessage(content=\"What did I tell you my name was?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -523,11 +358,7 @@
]
}
],
"source": [
"inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["inputs = [HumanMessage(content=\"what's the weather in sf now?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -561,10 +392,7 @@
]
}
],
"source": [
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -599,43 +427,7 @@
"id": "5454f436-d56e-4499-9381-06192aca1b56",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"from typing import Optional\n",
"\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"# Helper function to construct message asking for verification\n",
"def generate_verification_message(message: AIMessage) -> None:\n",
" \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n",
" serialized_tool_calls = json.dumps(\n",
" message.tool_calls,\n",
" indent=2,\n",
" )\n",
" return AIMessage(\n",
" content=(\n",
" \"I plan to invoke the following tools, do you approve?\\n\\n\"\n",
" \"Type 'y' if you do, anything else to stop.\\n\\n\"\n",
" f\"{serialized_tool_calls}\"\n",
" ),\n",
" id=message.id,\n",
" )\n",
"\n",
"\n",
"# Helper function to stream output from the graph\n",
"def stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n",
" \"\"\"Stream app, catching tool calls.\"\"\"\n",
" tool_call_message = None\n",
" for event in app.stream(inputs, thread, stream_mode=\"values\"):\n",
" message = event[\"messages\"][-1]\n",
" if isinstance(message, AIMessage) and message.tool_calls:\n",
" tool_call_message = message\n",
" else:\n",
" message.pretty_print()\n",
"\n",
" return tool_call_message"
]
"source": ["import json\nfrom typing import Optional\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to construct message asking for verification\ndef generate_verification_message(message: AIMessage) -> None:\n \"\"\"Generate \"verification message\" from message with tool calls.\"\"\"\n serialized_tool_calls = json.dumps(\n message.tool_calls,\n indent=2,\n )\n return AIMessage(\n content=(\n \"I plan to invoke the following tools, do you approve?\\n\\n\"\n \"Type 'y' if you do, anything else to stop.\\n\\n\"\n f\"{serialized_tool_calls}\"\n ),\n id=message.id,\n )\n\n\n# Helper function to stream output from the graph\ndef stream_app_catch_tool_calls(inputs, thread) -> Optional[AIMessage]:\n \"\"\"Stream app, catching tool calls.\"\"\"\n tool_call_message = None\n for event in app.stream(inputs, thread, stream_mode=\"values\"):\n message = event[\"messages\"][-1]\n if isinstance(message, AIMessage) and message.tool_calls:\n tool_call_message = message\n else:\n message.pretty_print()\n\n return tool_call_message"]
},
{
"cell_type": "code",
@@ -722,43 +514,7 @@
]
}
],
"source": [
"import uuid\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"\n",
"tool_call_message = stream_app_catch_tool_calls(\n",
" {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n",
" thread,\n",
")\n",
"\n",
"while tool_call_message:\n",
" verification_message = generate_verification_message(tool_call_message)\n",
" verification_message.pretty_print()\n",
" input_message = HumanMessage(input())\n",
" if input_message.content == \"exit\":\n",
" break\n",
" input_message.pretty_print()\n",
"\n",
" # First we update the state with the verification message and the input message.\n",
" # note that `generate_verification_message` sets the message ID to be the same\n",
" # as the ID from the original tool call message. Updating the state with this\n",
" # message will overwrite the previous tool call.\n",
" snapshot = app.get_state(thread)\n",
" snapshot.values[\"messages\"] += [verification_message, input_message]\n",
"\n",
" if input_message.content == \"y\":\n",
" tool_call_message.id = str(uuid.uuid4())\n",
" # If verified, we append the tool call message to the state\n",
" # and resume execution.\n",
" snapshot.values[\"messages\"] += [tool_call_message]\n",
" app.update_state(thread, snapshot.values, as_node=\"agent\")\n",
" else:\n",
" # Otherwise, resume execution from the input message.\n",
" app.update_state(thread, snapshot.values, as_node=\"__start__\")\n",
"\n",
" tool_call_message = stream_app_catch_tool_calls(None, thread)"
]
"source": ["import uuid\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\n\ntool_call_message = stream_app_catch_tool_calls(\n {\"messages\": [HumanMessage(\"what's the weather in sf now?\")]},\n thread,\n)\n\nwhile tool_call_message:\n verification_message = generate_verification_message(tool_call_message)\n verification_message.pretty_print()\n input_message = HumanMessage(input())\n if input_message.content == \"exit\":\n break\n input_message.pretty_print()\n\n # First we update the state with the verification message and the input message.\n # note that `generate_verification_message` sets the message ID to be the same\n # as the ID from the original tool call message. Updating the state with this\n # message will overwrite the previous tool call.\n snapshot = app.get_state(thread)\n snapshot.values[\"messages\"] += [verification_message, input_message]\n\n if input_message.content == \"y\":\n tool_call_message.id = str(uuid.uuid4())\n # If verified, we append the tool call message to the state\n # and resume execution.\n snapshot.values[\"messages\"] += [tool_call_message]\n app.update_state(thread, snapshot.values, as_node=\"agent\")\n else:\n # Otherwise, resume execution from the input message.\n app.update_state(thread, snapshot.values, as_node=\"__start__\")\n\n tool_call_message = stream_app_catch_tool_calls(None, thread)"]
},
{
"cell_type": "markdown",
@@ -779,34 +535,7 @@
"id": "03232f16-d6fe-46d0-afa0-a6f0d0bf16de",
"metadata": {},
"outputs": [],
"source": [
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
" tool_call_message: Optional[AIMessage]\n",
"\n",
"\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" if messages[-1].content == \"y\":\n",
" return {\n",
" \"messages\": [state[\"tool_call_message\"]],\n",
" \"tool_call_message\": None,\n",
" }\n",
" else:\n",
" response = model.invoke(messages)\n",
" if response.tool_calls:\n",
" verification_message = generate_verification_message(response)\n",
" response.id = str(uuid.uuid4())\n",
" return {\n",
" \"messages\": [verification_message],\n",
" \"tool_call_message\": response,\n",
" }\n",
" else:\n",
" return {\n",
" \"messages\": [response],\n",
" \"tool_call_message\": None,\n",
" }"
]
"source": ["class State(TypedDict):\n messages: Annotated[list, add_messages]\n tool_call_message: Optional[AIMessage]\n\n\ndef call_model(state):\n messages = state[\"messages\"]\n if messages[-1].content == \"y\":\n return {\n \"messages\": [state[\"tool_call_message\"]],\n \"tool_call_message\": None,\n }\n else:\n response = model.invoke(messages)\n if response.tool_calls:\n verification_message = generate_verification_message(response)\n response.id = str(uuid.uuid4())\n return {\n \"messages\": [verification_message],\n \"tool_call_message\": response,\n }\n else:\n return {\n \"messages\": [response],\n \"tool_call_message\": None,\n }"]
},
{
"cell_type": "markdown",
@@ -822,27 +551,7 @@
"id": "502dc688-c926-407e-8759-8c9e39eb4257",
"metadata": {},
"outputs": [],
"source": [
"workflow = StateGraph(State)\n",
"\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
" {\n",
" \"continue\": \"action\",\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"app = workflow.compile(checkpointer=memory)"
]
"source": ["workflow = StateGraph(State)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\nworkflow.add_edge(START, \"agent\")\n\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n {\n \"continue\": \"action\",\n \"end\": END,\n },\n)\n\nworkflow.add_edge(\"action\", \"agent\")\n\napp = workflow.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
@@ -875,13 +584,7 @@
]
}
],
"source": [
"thread = {\"configurable\": {\"thread_id\": \"4\"}}\n",
"\n",
"inputs = [HumanMessage(content=\"what's the weather in sf?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["thread = {\"configurable\": {\"thread_id\": \"4\"}}\n\ninputs = [HumanMessage(content=\"what's the weather in sf?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -914,11 +617,7 @@
]
}
],
"source": [
"inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["inputs = [HumanMessage(content=\"can you specify sf in CA?\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -949,11 +648,7 @@
]
}
],
"source": [
"inputs = [HumanMessage(content=\"y\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["inputs = [HumanMessage(content=\"y\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
}
],
"metadata": {
+6 -129
View File
@@ -32,10 +32,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
},
{
"cell_type": "markdown",
@@ -59,18 +56,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -86,10 +72,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -107,103 +90,7 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
"source": [
"# Set up the tool\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"from langgraph.graph import MessagesState\n",
"from langgraph.prebuilt import ToolNode\n",
"from langgraph.graph import END, StateGraph\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"tools = [search]\n",
"tool_node = ToolNode(tools)\n",
"\n",
"# Set up the model\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"model = model.bind_tools(tools)\n",
"\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Set up memory\n",
"memory = MemorySaver()\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"\n",
"# We add in `interrupt_before=[\"action\"]`\n",
"# This will add a breakpoint before the `action` node is called\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
"source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
},
{
"cell_type": "markdown",
@@ -239,14 +126,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -286,10 +166,7 @@
]
}
],
"source": [
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
}
],
"metadata": {
@@ -32,10 +32,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
},
{
"cell_type": "markdown",
@@ -59,18 +56,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -86,10 +72,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -107,103 +90,7 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
"source": [
"# Set up the tool\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"from langgraph.graph import MessagesState\n",
"from langgraph.prebuilt import ToolNode\n",
"from langgraph.graph import END, StateGraph\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"tools = [search]\n",
"tool_node = ToolNode(tools)\n",
"\n",
"# Set up the model\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"model = model.bind_tools(tools)\n",
"\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Set up memory\n",
"memory = MemorySaver()\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"\n",
"# We add in `interrupt_before=[\"action\"]`\n",
"# This will add a breakpoint before the `action` node is called\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
"source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
},
{
"cell_type": "markdown",
@@ -239,14 +126,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -276,25 +156,7 @@
"output_type": "execute_result"
}
],
"source": [
"# First, lets get the current state\n",
"current_state = app.get_state(thread)\n",
"\n",
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = current_state.values['messages'][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
"last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n",
"\n",
"# Let's now call `update_state` to pass in this message in the `messages` key\n",
"# This will get treated as any other update to the state\n",
"# It will get passed to the reducer function for the `messages` key\n",
"# That reducer function will use the ID of the message to update it\n",
"# It's important that it has the right ID! Otherwise it would get appended\n",
"# as a new message\n",
"app.update_state(thread, {\"messages\": last_message})"
]
"source": ["# First, lets get the current state\ncurrent_state = app.get_state(thread)\n\n# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = current_state.values['messages'][-1]\n\n# Let's now update the args for that tool call\nlast_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n\n# Let's now call `update_state` to pass in this message in the `messages` key\n# This will get treated as any other update to the state\n# It will get passed to the reducer function for the `messages` key\n# That reducer function will use the ID of the message to update it\n# It's important that it has the right ID! Otherwise it would get appended\n# as a new message\napp.update_state(thread, {\"messages\": last_message})"]
},
{
"cell_type": "markdown",
@@ -323,10 +185,7 @@
"output_type": "execute_result"
}
],
"source": [
"current_state = app.get_state(thread).values['messages'][-1].tool_calls\n",
"current_state"
]
"source": ["current_state = app.get_state(thread).values['messages'][-1].tool_calls\ncurrent_state"]
},
{
"cell_type": "markdown",
@@ -364,10 +223,7 @@
]
}
],
"source": [
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -375,7 +231,7 @@
"id": "78780afe-409d-46cd-a734-e82538cdd8de",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+17 -187
View File
@@ -39,10 +39,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
},
{
"cell_type": "markdown",
@@ -66,18 +63,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -93,10 +79,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -114,103 +97,7 @@
"id": "f5319e01",
"metadata": {},
"outputs": [],
"source": [
"# Set up the tool\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"from langgraph.graph import MessagesState\n",
"from langgraph.prebuilt import ToolNode\n",
"from langgraph.graph import END, StateGraph\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"tools = [search]\n",
"tool_node = ToolNode(tools)\n",
"\n",
"# Set up the model\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"model = model.bind_tools(tools)\n",
"\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Set up memory\n",
"memory = MemorySaver()\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"\n",
"# We add in `interrupt_before=[\"action\"]`\n",
"# This will add a breakpoint before the `action` node is called\n",
"app = workflow.compile(checkpointer=memory)"
]
"source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory)"]
},
{
"cell_type": "markdown",
@@ -264,14 +151,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"input_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\ninput_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -306,13 +186,7 @@
]
}
],
"source": [
"all_states = []\n",
"for state in app.get_state_history(config):\n",
" print(state)\n",
" all_states.append(state)\n",
" print(\"--\")"
]
"source": ["all_states = []\nfor state in app.get_state_history(config):\n print(state)\n all_states.append(state)\n print(\"--\")"]
},
{
"cell_type": "markdown",
@@ -330,9 +204,7 @@
"id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699",
"metadata": {},
"outputs": [],
"source": [
"to_replay = all_states[2]"
]
"source": ["to_replay = all_states[2]"]
},
{
"cell_type": "code",
@@ -352,9 +224,7 @@
"output_type": "execute_result"
}
],
"source": [
"to_replay.values"
]
"source": ["to_replay.values"]
},
{
"cell_type": "code",
@@ -373,9 +243,7 @@
"output_type": "execute_result"
}
],
"source": [
"to_replay.next"
]
"source": ["to_replay.next"]
},
{
"cell_type": "markdown",
@@ -400,11 +268,7 @@
]
}
],
"source": [
"for event in app.stream(None, to_replay.config):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["for event in app.stream(None, to_replay.config):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "markdown",
@@ -424,18 +288,7 @@
"id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f",
"metadata": {},
"outputs": [],
"source": [
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = to_replay.values['messages'][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
"last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n",
"\n",
"branch_config = app.update_state(\n",
" to_replay.config, {\"messages\": [last_message]},\n",
")"
]
"source": ["# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = to_replay.values['messages'][-1]\n\n# Let's now update the args for that tool call\nlast_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n\nbranch_config = app.update_state(\n to_replay.config, {\"messages\": [last_message]},\n)"]
},
{
"cell_type": "markdown",
@@ -460,11 +313,7 @@
]
}
],
"source": [
"for event in app.stream(None, branch_config):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["for event in app.stream(None, branch_config):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "markdown",
@@ -480,20 +329,7 @@
"id": "01abb480-df55-4eba-a2be-cf9372b60b54",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"\n",
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = to_replay.values['messages'][-1]\n",
"\n",
"# Let's now get the ID for the last message, and create a new message with that ID.\n",
"new_message = AIMessage(content=\"its warm!\", id=last_message.id)\n",
"\n",
"branch_config = app.update_state(\n",
" to_replay.config, {\"messages\": [new_message]},\n",
")"
]
"source": ["from langchain_core.messages import AIMessage\n\n# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = to_replay.values['messages'][-1]\n\n# Let's now get the ID for the last message, and create a new message with that ID.\nnew_message = AIMessage(content=\"its warm!\", id=last_message.id)\n\nbranch_config = app.update_state(\n to_replay.config, {\"messages\": [new_message]},\n)"]
},
{
"cell_type": "code",
@@ -501,9 +337,7 @@
"id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641",
"metadata": {},
"outputs": [],
"source": [
"branch_state = app.get_state(branch_config)"
]
"source": ["branch_state = app.get_state(branch_config)"]
},
{
"cell_type": "code",
@@ -523,9 +357,7 @@
"output_type": "execute_result"
}
],
"source": [
"branch_state.values"
]
"source": ["branch_state.values"]
},
{
"cell_type": "code",
@@ -544,9 +376,7 @@
"output_type": "execute_result"
}
],
"source": [
"branch_state.next"
]
"source": ["branch_state.next"]
},
{
"cell_type": "markdown",
@@ -562,7 +392,7 @@
"id": "74a7a5ed-0c14-4883-a16b-d70aaf40f7ea",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -40,10 +40,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
},
{
"cell_type": "markdown",
@@ -67,18 +64,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -94,10 +80,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -115,144 +98,7 @@
"id": "f5319e01",
"metadata": {},
"outputs": [],
"source": [
"# Set up the state\n",
"from langgraph.graph import MessagesState\n",
"\n",
"# Set up the tool\n",
"# We will have one real tool - a search tool\n",
"# We'll also have one \"fake\" tool - a \"ask_human\" tool\n",
"# Here we define any ACTUAL tools\n",
"from langchain_core.tools import tool\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]\n",
"tool_node = ToolNode(tools)\n",
"\n",
"# Set up the model\n",
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"\n",
"\n",
"# We are going \"bind\" all tools to the model\n",
"# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n",
"# Since `bind_tools` takes in tools but also just tool definitions,\n",
"# We can define a tool definition for `ask_human`\n",
"\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"\n",
"class AskHuman(BaseModel):\n",
" \"\"\"Ask the human a question\"\"\"\n",
" question: str\n",
"\n",
"\n",
"model = model.bind_tools(tools + [AskHuman])\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # If tool call is asking Human, we return that node\n",
" # You could also add logic here to let some system know that there's something that requires Human input\n",
" # For example, send a slack message, etc\n",
" elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n",
" return \"ask_human\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"\n",
"# We define a fake node to ask the human\n",
"def ask_human(state):\n",
" pass\n",
"\n",
"# Build the graph\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"# Define the three nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"workflow.add_node(\"ask_human\", ask_human)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # We may ask the human\n",
" \"ask_human\": \"ask_human\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# After we get back the human response, we go back to the agent\n",
"workflow.add_edge(\"ask_human\", \"agent\")\n",
"\n",
"# Set up memory\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"# We add a breakpoint BEFORE the `ask_human` node so it never executes\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])"
]
"source": ["# Set up the state\nfrom langgraph.graph import MessagesState, START\n\n# Set up the tool\n# We will have one real tool - a search tool\n# We'll also have one \"fake\" tool - a \"ask_human\" tool\n# Here we define any ACTUAL tools\nfrom langchain_core.tools import tool\nfrom langgraph.prebuilt import ToolNode\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\nfrom langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n\n\n# We are going \"bind\" all tools to the model\n# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n# Since `bind_tools` takes in tools but also just tool definitions,\n# We can define a tool definition for `ask_human`\n\nfrom langchain_core.pydantic_v1 import BaseModel\n\nclass AskHuman(BaseModel):\n \"\"\"Ask the human a question\"\"\"\n question: str\n\n\nmodel = model.bind_tools(tools + [AskHuman])\n\n# Define nodes and conditional edges\n\nfrom langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # If tool call is asking Human, we return that node\n # You could also add logic here to let some system know that there's something that requires Human input\n # For example, send a slack message, etc\n elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n return \"ask_human\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n\n# We define a fake node to ask the human\ndef ask_human(state):\n pass\n\n# Build the graph\n\nfrom langgraph.graph import END, StateGraph\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the three nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\nworkflow.add_node(\"ask_human\", ask_human)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # We may ask the human\n \"ask_human\": \"ask_human\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After we get back the human response, we go back to the agent\nworkflow.add_edge(\"ask_human\", \"agent\")\n\n# Set up memory\nfrom langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n# We add a breakpoint BEFORE the `ask_human` node so it never executes\napp = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])"]
},
{
"cell_type": "markdown",
@@ -288,14 +134,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -324,29 +163,7 @@
"output_type": "execute_result"
}
],
"source": [
"tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n",
"\n",
"# We now create the tool call with the id and the response we want\n",
"tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n",
"\n",
"# # This is equivalent to the below, either one works\n",
"# from langchain_core.messages import ToolMessage\n",
"# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n",
"\n",
"# We now update the state\n",
"# Notice that we are also specifying `as_node=\"ask_human\"`\n",
"# This will apply this update as this node,\n",
"# which will make it so that afterwards it continues as normal\n",
"app.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n",
"\n",
"# We can check the state\n",
"# We can see that the state currently has the `agent` node next\n",
"# This is based on how we define our graph, \n",
"# where after the `ask_human` node goes (which we just triggered)\n",
"# there is an edge to the `agent` node\n",
"app.get_state(config).next"
]
"source": ["tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n\n# We now create the tool call with the id and the response we want\ntool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n\n# # This is equivalent to the below, either one works\n# from langchain_core.messages import ToolMessage\n# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n\n# We now update the state\n# Notice that we are also specifying `as_node=\"ask_human\"`\n# This will apply this update as this node,\n# which will make it so that afterwards it continues as normal\napp.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n\n# We can check the state\n# We can see that the state currently has the `agent` node next\n# This is based on how we define our graph, \n# where after the `ask_human` node goes (which we just triggered)\n# there is an edge to the `agent` node\napp.get_state(config).next"]
},
{
"cell_type": "markdown",
@@ -388,10 +205,7 @@
]
}
],
"source": [
"for event in app.stream(None, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["for event in app.stream(None, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -399,7 +213,7 @@
"id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because it is too large Load Diff
+19 -439
View File
@@ -37,11 +37,7 @@
"id": "dcc9159b-cc8c-426d-9670-3e8ada06723f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U --quiet langchain langgraph langchain_openai\n",
"%pip install -U --quiet tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install -U --quiet langchain langgraph langchain_openai\n%pip install -U --quiet tavily-python"]
},
{
"cell_type": "code",
@@ -49,27 +45,7 @@
"id": "a177ecc9-0c96-460f-9b39-9c1ce54754f1",
"metadata": {},
"outputs": [],
"source": [
"from __future__ import annotations # noqa: F404\n",
"\n",
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str) -> None:\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"# Optional: Configure tracing to visualize and debug the agent\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"LATS\"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")"
]
"source": ["from __future__ import annotations # noqa: F404\n\nimport getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"LATS\"\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -93,134 +69,7 @@
"id": "54c6f319-3966-4f66-aa7b-50e249189111",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"from collections import deque\n",
"from typing import Optional\n",
"\n",
"from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage\n",
"\n",
"\n",
"class Node:\n",
" def __init__(\n",
" self,\n",
" messages: list[BaseMessage],\n",
" reflection: Reflection,\n",
" parent: Optional[Node] = None,\n",
" ):\n",
" self.messages = messages\n",
" self.parent = parent\n",
" self.children = []\n",
" self.value = 0\n",
" self.visits = 0\n",
" self.reflection = reflection\n",
" self.depth = parent.depth + 1 if parent is not None else 1\n",
" self._is_solved = reflection.found_solution if reflection else False\n",
" if self._is_solved:\n",
" self._mark_tree_as_solved()\n",
" self.backpropagate(reflection.normalized_score)\n",
"\n",
" def __repr__(self) -> str:\n",
" return (\n",
" f\"<Node value={self.value}, visits={self.visits},\"\n",
" f\" solution={self.messages} reflection={self.reflection}/>\"\n",
" )\n",
"\n",
" @property\n",
" def is_solved(self):\n",
" \"\"\"If any solutions exist, we can end the search.\"\"\"\n",
" return self._is_solved\n",
"\n",
" @property\n",
" def is_terminal(self):\n",
" return not self.children\n",
"\n",
" @property\n",
" def best_child(self):\n",
" \"\"\"Select the child with the highest UCT to search next.\"\"\"\n",
" if not self.children:\n",
" return None\n",
" all_nodes = self._get_all_children()\n",
" return max(all_nodes, key=lambda child: child.upper_confidence_bound())\n",
"\n",
" @property\n",
" def best_child_score(self):\n",
" \"\"\"Return the child with the highest value.\"\"\"\n",
" if not self.children:\n",
" return None\n",
" return max(self.children, key=lambda child: int(child.is_solved) * child.value)\n",
"\n",
" @property\n",
" def height(self) -> int:\n",
" \"\"\"Check for how far we've rolled out the tree.\"\"\"\n",
" if self.children:\n",
" return 1 + max([child.height for child in self.children])\n",
" return 1\n",
"\n",
" def upper_confidence_bound(self, exploration_weight=1.0):\n",
" \"\"\"Return the UCT score. This helps balance exploration vs. exploitation of a branch.\"\"\"\n",
" if self.parent is None:\n",
" raise ValueError(\"Cannot obtain UCT from root node\")\n",
" if self.visits == 0:\n",
" return self.value\n",
" # Encourages exploitation of high-value trajectories\n",
" average_reward = self.value / self.visits\n",
" # Encourages exploration of less-visited trajectories\n",
" exploration_term = math.sqrt(math.log(self.parent.visits) / self.visits)\n",
" return average_reward + exploration_weight * exploration_term\n",
"\n",
" def backpropagate(self, reward: float):\n",
" \"\"\"Update the score of this node and its parents.\"\"\"\n",
" node = self\n",
" while node:\n",
" node.visits += 1\n",
" node.value = (node.value * (node.visits - 1) + reward) / node.visits\n",
" node = node.parent\n",
"\n",
" def get_messages(self, include_reflections: bool = True):\n",
" if include_reflections:\n",
" return self.messages + [self.reflection.as_message()]\n",
" return self.messages\n",
"\n",
" def get_trajectory(self, include_reflections: bool = True) -> list[BaseMessage]:\n",
" \"\"\"Get messages representing this search branch.\"\"\"\n",
" messages = []\n",
" node = self\n",
" while node:\n",
" messages.extend(\n",
" node.get_messages(include_reflections=include_reflections)[::-1]\n",
" )\n",
" node = node.parent\n",
" # Reverse the final back-tracked trajectory to return in the correct order\n",
" return messages[::-1] # root solution, reflection, child 1, ...\n",
"\n",
" def _get_all_children(self):\n",
" all_nodes = []\n",
" nodes = deque()\n",
" nodes.append(self)\n",
" while nodes:\n",
" node = nodes.popleft()\n",
" all_nodes.extend(node.children)\n",
" for n in node.children:\n",
" nodes.append(n)\n",
" return all_nodes\n",
"\n",
" def get_best_solution(self):\n",
" \"\"\"Return the best solution from within the current sub-tree.\"\"\"\n",
" all_nodes = [self] + self._get_all_children()\n",
" best_node = max(\n",
" all_nodes,\n",
" # We filter out all non-terminal, non-solution trajectories\n",
" key=lambda node: int(node.is_terminal and node.is_solved) * node.value,\n",
" )\n",
" return best_node\n",
"\n",
" def _mark_tree_as_solved(self):\n",
" parent = self.parent\n",
" while parent:\n",
" parent._is_solved = True\n",
" parent = parent.parent"
]
"source": ["import math\nfrom collections import deque\nfrom typing import Optional\n\nfrom langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage\n\n\nclass Node:\n def __init__(\n self,\n messages: list[BaseMessage],\n reflection: Reflection,\n parent: Optional[Node] = None,\n ):\n self.messages = messages\n self.parent = parent\n self.children = []\n self.value = 0\n self.visits = 0\n self.reflection = reflection\n self.depth = parent.depth + 1 if parent is not None else 1\n self._is_solved = reflection.found_solution if reflection else False\n if self._is_solved:\n self._mark_tree_as_solved()\n self.backpropagate(reflection.normalized_score)\n\n def __repr__(self) -> str:\n return (\n f\"<Node value={self.value}, visits={self.visits},\"\n f\" solution={self.messages} reflection={self.reflection}/>\"\n )\n\n @property\n def is_solved(self):\n \"\"\"If any solutions exist, we can end the search.\"\"\"\n return self._is_solved\n\n @property\n def is_terminal(self):\n return not self.children\n\n @property\n def best_child(self):\n \"\"\"Select the child with the highest UCT to search next.\"\"\"\n if not self.children:\n return None\n all_nodes = self._get_all_children()\n return max(all_nodes, key=lambda child: child.upper_confidence_bound())\n\n @property\n def best_child_score(self):\n \"\"\"Return the child with the highest value.\"\"\"\n if not self.children:\n return None\n return max(self.children, key=lambda child: int(child.is_solved) * child.value)\n\n @property\n def height(self) -> int:\n \"\"\"Check for how far we've rolled out the tree.\"\"\"\n if self.children:\n return 1 + max([child.height for child in self.children])\n return 1\n\n def upper_confidence_bound(self, exploration_weight=1.0):\n \"\"\"Return the UCT score. This helps balance exploration vs. exploitation of a branch.\"\"\"\n if self.parent is None:\n raise ValueError(\"Cannot obtain UCT from root node\")\n if self.visits == 0:\n return self.value\n # Encourages exploitation of high-value trajectories\n average_reward = self.value / self.visits\n # Encourages exploration of less-visited trajectories\n exploration_term = math.sqrt(math.log(self.parent.visits) / self.visits)\n return average_reward + exploration_weight * exploration_term\n\n def backpropagate(self, reward: float):\n \"\"\"Update the score of this node and its parents.\"\"\"\n node = self\n while node:\n node.visits += 1\n node.value = (node.value * (node.visits - 1) + reward) / node.visits\n node = node.parent\n\n def get_messages(self, include_reflections: bool = True):\n if include_reflections:\n return self.messages + [self.reflection.as_message()]\n return self.messages\n\n def get_trajectory(self, include_reflections: bool = True) -> list[BaseMessage]:\n \"\"\"Get messages representing this search branch.\"\"\"\n messages = []\n node = self\n while node:\n messages.extend(\n node.get_messages(include_reflections=include_reflections)[::-1]\n )\n node = node.parent\n # Reverse the final back-tracked trajectory to return in the correct order\n return messages[::-1] # root solution, reflection, child 1, ...\n\n def _get_all_children(self):\n all_nodes = []\n nodes = deque()\n nodes.append(self)\n while nodes:\n node = nodes.popleft()\n all_nodes.extend(node.children)\n for n in node.children:\n nodes.append(n)\n return all_nodes\n\n def get_best_solution(self):\n \"\"\"Return the best solution from within the current sub-tree.\"\"\"\n all_nodes = [self] + self._get_all_children()\n best_node = max(\n all_nodes,\n # We filter out all non-terminal, non-solution trajectories\n key=lambda node: int(node.is_terminal and node.is_solved) * node.value,\n )\n return best_node\n\n def _mark_tree_as_solved(self):\n parent = self.parent\n while parent:\n parent._is_solved = True\n parent = parent.parent"]
},
{
"cell_type": "markdown",
@@ -238,16 +87,7 @@
"id": "e10c94ba-9daa-4899-97ce-4f28428c2c38",
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class TreeState(TypedDict):\n",
" # The full tree\n",
" root: Node\n",
" # The original input\n",
" input: str"
]
"source": ["from typing_extensions import TypedDict\n\n\nclass TreeState(TypedDict):\n # The full tree\n root: Node\n # The original input\n input: str"]
},
{
"cell_type": "markdown",
@@ -270,11 +110,7 @@
"id": "48738896-42ac-47eb-b482-0d4d4dd86c87",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4o\")"
]
"source": ["from langchain_openai import ChatOpenAI\n\nllm = ChatOpenAI(model=\"gpt-4o\")"]
},
{
"cell_type": "markdown",
@@ -292,17 +128,7 @@
"id": "55c2aff3-f454-43da-8f45-1a3d46523cd5",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n",
"\n",
"from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation\n",
"\n",
"search = TavilySearchAPIWrapper()\n",
"tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)\n",
"tools = [tavily_tool]\n",
"tool_executor = ToolExecutor(tools=tools)"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nfrom langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)\ntools = [tavily_tool]\ntool_executor = ToolExecutor(tools=tools)"]
},
{
"cell_type": "markdown",
@@ -321,68 +147,7 @@
"id": "ddfd1750-c265-4b29-b505-83b1c5e2d30e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.output_parsers.openai_tools import (\n",
" JsonOutputToolsParser,\n",
" PydanticToolsParser,\n",
")\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_core.runnables import chain as as_runnable\n",
"\n",
"\n",
"class Reflection(BaseModel):\n",
" reflections: str = Field(\n",
" description=\"The critique and reflections on the sufficiency, superfluency,\"\n",
" \" and general quality of the response\"\n",
" )\n",
" score: int = Field(\n",
" description=\"Score from 0-10 on the quality of the candidate response.\",\n",
" gte=0,\n",
" lte=10,\n",
" )\n",
" found_solution: bool = Field(\n",
" description=\"Whether the response has fully solved the question or task.\"\n",
" )\n",
"\n",
" def as_message(self):\n",
" return HumanMessage(\n",
" content=f\"Reasoning: {self.reflections}\\nScore: {self.score}\"\n",
" )\n",
"\n",
" @property\n",
" def normalized_score(self) -> float:\n",
" return self.score / 10.0\n",
"\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"Reflect and grade the assistant response to the user question below.\",\n",
" ),\n",
" (\"user\", \"{input}\"),\n",
" MessagesPlaceholder(variable_name=\"candidate\"),\n",
" ]\n",
")\n",
"\n",
"reflection_llm_chain = (\n",
" prompt\n",
" | llm.bind_tools(tools=[Reflection], tool_choice=\"Reflection\").with_config(\n",
" run_name=\"Reflection\"\n",
" )\n",
" | PydanticToolsParser(tools=[Reflection])\n",
")\n",
"\n",
"\n",
"@as_runnable\n",
"def reflection_chain(inputs) -> Reflection:\n",
" tool_choices = reflection_llm_chain.invoke(inputs)\n",
" reflection = tool_choices[0]\n",
" if not isinstance(inputs[\"candidate\"][-1], AIMessage):\n",
" reflection.found_solution = False\n",
" return reflection"
]
"source": ["from langchain_core.output_parsers.openai_tools import (\n JsonOutputToolsParser,\n PydanticToolsParser,\n)\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_core.runnables import chain as as_runnable\n\n\nclass Reflection(BaseModel):\n reflections: str = Field(\n description=\"The critique and reflections on the sufficiency, superfluency,\"\n \" and general quality of the response\"\n )\n score: int = Field(\n description=\"Score from 0-10 on the quality of the candidate response.\",\n gte=0,\n lte=10,\n )\n found_solution: bool = Field(\n description=\"Whether the response has fully solved the question or task.\"\n )\n\n def as_message(self):\n return HumanMessage(\n content=f\"Reasoning: {self.reflections}\\nScore: {self.score}\"\n )\n\n @property\n def normalized_score(self) -> float:\n return self.score / 10.0\n\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"Reflect and grade the assistant response to the user question below.\",\n ),\n (\"user\", \"{input}\"),\n MessagesPlaceholder(variable_name=\"candidate\"),\n ]\n)\n\nreflection_llm_chain = (\n prompt\n | llm.bind_tools(tools=[Reflection], tool_choice=\"Reflection\").with_config(\n run_name=\"Reflection\"\n )\n | PydanticToolsParser(tools=[Reflection])\n)\n\n\n@as_runnable\ndef reflection_chain(inputs) -> Reflection:\n tool_choices = reflection_llm_chain.invoke(inputs)\n reflection = tool_choices[0]\n if not isinstance(inputs[\"candidate\"][-1], AIMessage):\n reflection.found_solution = False\n return reflection"]
},
{
"cell_type": "markdown",
@@ -400,29 +165,7 @@
"id": "72fc5363-f0f3-4362-8499-14eb583bd75b",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.prompt_values import ChatPromptValue\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"prompt_template = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are an AI assistant.\",\n",
" ),\n",
" (\"user\", \"{input}\"),\n",
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
" ]\n",
")\n",
"\n",
"\n",
"initial_answer_chain = prompt_template | llm.bind_tools(tools=tools).with_config(\n",
" run_name=\"GenerateInitialCandidate\"\n",
")\n",
"\n",
"\n",
"parser = JsonOutputToolsParser(return_id=True)"
]
"source": ["from langchain_core.prompt_values import ChatPromptValue\nfrom langchain_core.runnables import RunnableConfig\n\nprompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an AI assistant.\",\n ),\n (\"user\", \"{input}\"),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ninitial_answer_chain = prompt_template | llm.bind_tools(tools=tools).with_config(\n run_name=\"GenerateInitialCandidate\"\n)\n\n\nparser = JsonOutputToolsParser(return_id=True)"]
},
{
"cell_type": "code",
@@ -441,12 +184,7 @@
"output_type": "execute_result"
}
],
"source": [
"initial_response = initial_answer_chain.invoke(\n",
" {\"input\": \"Write a research report on lithium pollution.\"}\n",
")\n",
"initial_response"
]
"source": ["initial_response = initial_answer_chain.invoke(\n {\"input\": \"Write a research report on lithium pollution.\"}\n)\ninitial_response"]
},
{
"cell_type": "markdown",
@@ -464,31 +202,7 @@
"id": "5b6b173c-78f5-4ae1-80b3-28c80e68f5c5",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"\n",
"\n",
"# Define the node we will add to the graph\n",
"def generate_initial_response(state: TreeState) -> dict:\n",
" \"\"\"Generate the initial candidate response.\"\"\"\n",
" res = initial_answer_chain.invoke({\"input\": state[\"input\"]})\n",
" parsed = parser.invoke(res)\n",
" tool_responses = tool_executor.batch(\n",
" [ToolInvocation(tool=r[\"type\"], tool_input=r[\"args\"]) for r in parsed]\n",
" )\n",
" output_messages = [res] + [\n",
" ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n",
" for resp, tool_call in zip(tool_responses, parsed)\n",
" ]\n",
" reflection = reflection_chain.invoke(\n",
" {\"input\": state[\"input\"], \"candidate\": output_messages}\n",
" )\n",
" root = Node(output_messages, reflection=reflection)\n",
" return {\n",
" **state,\n",
" \"root\": root,\n",
" }"
]
"source": ["import json\n\n\n# Define the node we will add to the graph\ndef generate_initial_response(state: TreeState) -> dict:\n \"\"\"Generate the initial candidate response.\"\"\"\n res = initial_answer_chain.invoke({\"input\": state[\"input\"]})\n parsed = parser.invoke(res)\n tool_responses = tool_executor.batch(\n [ToolInvocation(tool=r[\"type\"], tool_input=r[\"args\"]) for r in parsed]\n )\n output_messages = [res] + [\n ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n for resp, tool_call in zip(tool_responses, parsed)\n ]\n reflection = reflection_chain.invoke(\n {\"input\": state[\"input\"], \"candidate\": output_messages}\n )\n root = Node(output_messages, reflection=reflection)\n return {\n **state,\n \"root\": root,\n }"]
},
{
"cell_type": "markdown",
@@ -506,26 +220,7 @@
"id": "550bff9a-86aa-43ad-ad98-506e97c122d2",
"metadata": {},
"outputs": [],
"source": [
"# This generates N candidate values\n",
"# for a single input to sample actions from the environment\n",
"\n",
"\n",
"def generate_candidates(messages: ChatPromptValue, config: RunnableConfig):\n",
" n = config[\"configurable\"].get(\"N\", 5)\n",
" bound_kwargs = llm.bind_tools(tools=tools).kwargs\n",
" chat_result = llm.generate(\n",
" [messages.to_messages()],\n",
" n=n,\n",
" callbacks=config[\"callbacks\"],\n",
" run_name=\"GenerateCandidates\",\n",
" **bound_kwargs,\n",
" )\n",
" return [gen.message for gen in chat_result.generations[0]]\n",
"\n",
"\n",
"expansion_chain = prompt_template | generate_candidates"
]
"source": ["# This generates N candidate values\n# for a single input to sample actions from the environment\n\n\ndef generate_candidates(messages: ChatPromptValue, config: RunnableConfig):\n n = config[\"configurable\"].get(\"N\", 5)\n bound_kwargs = llm.bind_tools(tools=tools).kwargs\n chat_result = llm.generate(\n [messages.to_messages()],\n n=n,\n callbacks=config[\"callbacks\"],\n run_name=\"GenerateCandidates\",\n **bound_kwargs,\n )\n return [gen.message for gen in chat_result.generations[0]]\n\n\nexpansion_chain = prompt_template | generate_candidates"]
},
{
"cell_type": "code",
@@ -548,10 +243,7 @@
"output_type": "execute_result"
}
],
"source": [
"res = expansion_chain.invoke({\"input\": \"Write a research report on lithium pollution.\"})\n",
"res"
]
"source": ["res = expansion_chain.invoke({\"input\": \"Write a research report on lithium pollution.\"})\nres"]
},
{
"cell_type": "markdown",
@@ -570,55 +262,7 @@
"id": "d32af859-53e8-46be-8182-7d522be31f54",
"metadata": {},
"outputs": [],
"source": [
"from collections import defaultdict\n",
"\n",
"\n",
"def expand(state: TreeState, config: RunnableConfig) -> dict:\n",
" \"\"\"Starting from the \"best\" node in the tree, generate N candidates for the next step.\"\"\"\n",
" root = state[\"root\"]\n",
" best_candidate: Node = root.best_child if root.children else root\n",
" messages = best_candidate.get_trajectory()\n",
" # Generate N candidates from the single child candidate\n",
" new_candidates = expansion_chain.invoke(\n",
" {\"input\": state[\"input\"], \"messages\": messages}, config\n",
" )\n",
" parsed = parser.batch(new_candidates)\n",
" flattened = [\n",
" (i, tool_call)\n",
" for i, tool_calls in enumerate(parsed)\n",
" for tool_call in tool_calls\n",
" ]\n",
" tool_responses = tool_executor.batch(\n",
" [\n",
" ToolInvocation(tool=tool_call[\"type\"], tool_input=tool_call[\"args\"])\n",
" for _, tool_call in flattened\n",
" ]\n",
" )\n",
" collected_responses = defaultdict(list)\n",
" for (i, tool_call), resp in zip(flattened, tool_responses):\n",
" collected_responses[i].append(\n",
" ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n",
" )\n",
" output_messages = []\n",
" for i, candidate in enumerate(new_candidates):\n",
" output_messages.append([candidate] + collected_responses[i])\n",
"\n",
" # Reflect on each candidate\n",
" # For tasks with external validation, you'd add that here.\n",
" reflections = reflection_chain.batch(\n",
" [{\"input\": state[\"input\"], \"candidate\": msges} for msges in output_messages],\n",
" config,\n",
" )\n",
" # Grow tree\n",
" child_nodes = [\n",
" Node(cand, parent=best_candidate, reflection=reflection)\n",
" for cand, reflection in zip(output_messages, reflections)\n",
" ]\n",
" best_candidate.children.extend(child_nodes)\n",
" # We have already extended the tree directly, so we just return the state\n",
" return state"
]
"source": ["from collections import defaultdict\n\n\ndef expand(state: TreeState, config: RunnableConfig) -> dict:\n \"\"\"Starting from the \"best\" node in the tree, generate N candidates for the next step.\"\"\"\n root = state[\"root\"]\n best_candidate: Node = root.best_child if root.children else root\n messages = best_candidate.get_trajectory()\n # Generate N candidates from the single child candidate\n new_candidates = expansion_chain.invoke(\n {\"input\": state[\"input\"], \"messages\": messages}, config\n )\n parsed = parser.batch(new_candidates)\n flattened = [\n (i, tool_call)\n for i, tool_calls in enumerate(parsed)\n for tool_call in tool_calls\n ]\n tool_responses = tool_executor.batch(\n [\n ToolInvocation(tool=tool_call[\"type\"], tool_input=tool_call[\"args\"])\n for _, tool_call in flattened\n ]\n )\n collected_responses = defaultdict(list)\n for (i, tool_call), resp in zip(flattened, tool_responses):\n collected_responses[i].append(\n ToolMessage(content=json.dumps(resp), tool_call_id=tool_call[\"id\"])\n )\n output_messages = []\n for i, candidate in enumerate(new_candidates):\n output_messages.append([candidate] + collected_responses[i])\n\n # Reflect on each candidate\n # For tasks with external validation, you'd add that here.\n reflections = reflection_chain.batch(\n [{\"input\": state[\"input\"], \"candidate\": msges} for msges in output_messages],\n config,\n )\n # Grow tree\n child_nodes = [\n Node(cand, parent=best_candidate, reflection=reflection)\n for cand, reflection in zip(output_messages, reflections)\n ]\n best_candidate.children.extend(child_nodes)\n # We have already extended the tree directly, so we just return the state\n return state"]
},
{
"cell_type": "markdown",
@@ -636,41 +280,7 @@
"id": "8aec0f20-f978-4df0-8900-e3a1f0544f6d",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"\n",
"def should_loop(state: TreeState) -> Literal[\"expand\", \"__end__\"]:\n",
" \"\"\"Determine whether to continue the tree search.\"\"\"\n",
" root = state[\"root\"]\n",
" if root.is_solved:\n",
" return END\n",
" if root.height > 5:\n",
" return END\n",
" return \"expand\"\n",
"\n",
"\n",
"builder = StateGraph(TreeState)\n",
"builder.add_node(\"start\", generate_initial_response)\n",
"builder.add_node(\"expand\", expand)\n",
"builder.set_entry_point(\"start\")\n",
"\n",
"\n",
"builder.add_conditional_edges(\n",
" \"start\",\n",
" # Either expand/rollout or finish\n",
" should_loop,\n",
")\n",
"builder.add_conditional_edges(\n",
" \"expand\",\n",
" # Either continue to rollout or finish\n",
" should_loop,\n",
")\n",
"\n",
"graph = builder.compile()"
]
"source": ["from typing import Literal\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef should_loop(state: TreeState) -> Literal[\"expand\", \"__end__\"]:\n \"\"\"Determine whether to continue the tree search.\"\"\"\n root = state[\"root\"]\n if root.is_solved:\n return END\n if root.height > 5:\n return END\n return \"expand\"\n\n\nbuilder = StateGraph(TreeState)\nbuilder.add_node(\"start\", generate_initial_response)\nbuilder.add_node(\"expand\", expand)\nbuilder.add_edge(START, \"start\")\n\n\nbuilder.add_conditional_edges(\n \"start\",\n # Either expand/rollout or finish\n should_loop,\n)\nbuilder.add_conditional_edges(\n \"expand\",\n # Either continue to rollout or finish\n should_loop,\n)\n\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -690,11 +300,7 @@
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import Image\n",
"\n",
"Image(graph.get_graph().draw_mermaid_png())"
]
"source": ["from IPython.display import Image\n\nImage(graph.get_graph().draw_mermaid_png())"]
},
{
"cell_type": "markdown",
@@ -723,16 +329,7 @@
]
}
],
"source": [
"question = \"Generate a table with the average size and weight, as well as the oldest recorded instance for each of the top 5 most common birds.\"\n",
"last_step = None\n",
"for step in graph.stream({\"input\": question}):\n",
" last_step = step\n",
" step_name, step_state = next(iter(step.items()))\n",
" print(step_name)\n",
" print(\"rolled out: \", step_state[\"root\"].height)\n",
" print(\"---\")"
]
"source": ["question = \"Generate a table with the average size and weight, as well as the oldest recorded instance for each of the top 5 most common birds.\"\nlast_step = None\nfor step in graph.stream({\"input\": question}):\n last_step = step\n step_name, step_state = next(iter(step.items()))\n print(step_name)\n print(\"rolled out: \", step_state[\"root\"].height)\n print(\"---\")"]
},
{
"cell_type": "code",
@@ -786,11 +383,7 @@
]
}
],
"source": [
"solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\n",
"best_trajectory = solution_node.get_trajectory(include_reflections=False)\n",
"print(best_trajectory[-1].content)"
]
"source": ["solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\nbest_trajectory = solution_node.get_trajectory(include_reflections=False)\nprint(best_trajectory[-1].content)"]
},
{
"cell_type": "code",
@@ -814,16 +407,7 @@
]
}
],
"source": [
"question = \"Write out magnus carlson series of moves in his game against Alireza Firouzja and propose an alternate strategy\"\n",
"last_step = None\n",
"for step in graph.stream({\"input\": question}):\n",
" last_step = step\n",
" step_name, step_state = next(iter(step.items()))\n",
" print(step_name)\n",
" print(\"rolled out: \", step_state[\"root\"].height)\n",
" print(\"---\")"
]
"source": ["question = \"Write out magnus carlson series of moves in his game against Alireza Firouzja and propose an alternate strategy\"\nlast_step = None\nfor step in graph.stream({\"input\": question}):\n last_step = step\n step_name, step_state = next(iter(step.items()))\n print(step_name)\n print(\"rolled out: \", step_state[\"root\"].height)\n print(\"---\")"]
},
{
"cell_type": "code",
@@ -886,11 +470,7 @@
]
}
],
"source": [
"solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\n",
"best_trajectory = solution_node.get_trajectory(include_reflections=False)\n",
"print(best_trajectory[-1].content)"
]
"source": ["solution_node = last_step[\"expand\"][\"root\"].get_best_solution()\nbest_trajectory = solution_node.get_trajectory(include_reflections=False)\nprint(best_trajectory[-1].content)"]
},
{
"cell_type": "markdown",
+24 -218
View File
@@ -38,9 +38,7 @@
]
}
],
"source": [
"!%pip install --quiet -U langgraph langchain langchain_openai tavily-pythonvily-python"
]
"source": ["!%pip install --quiet -U langgraph langchain langchain_openai tavily-pythonvily-python"]
},
{
"cell_type": "markdown",
@@ -65,13 +63,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\n",
"os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"
]
"source": ["import getpass\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")\nos.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Tavily API Key:\")"]
},
{
"cell_type": "markdown",
@@ -87,10 +79,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -110,11 +99,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"tools = [TavilySearchResults(max_results=1)]"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=1)]"]
},
{
"cell_type": "markdown",
@@ -131,11 +116,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -159,11 +140,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "markdown",
@@ -180,9 +157,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -217,17 +192,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" last_message = state[\"messages\"][-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\""
]
"source": ["# Define the function that determines whether to continue or not\ndef should_continue(state):\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -245,107 +210,7 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated, TypedDict\n",
"\n",
"from langchain_core.messages import (\n",
" AIMessage,\n",
" AnyMessage,\n",
" HumanMessage,\n",
" SystemMessage,\n",
" ToolMessage,\n",
")\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.managed.few_shot import FewShotExamples\n",
"\n",
"\n",
"class BaseState(TypedDict):\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
" examples: Annotated[list, FewShotExamples]\n",
"\n",
"\n",
"def _render_message(m):\n",
" if isinstance(m, HumanMessage):\n",
" return \"Human: \" + m.content\n",
" elif isinstance(m, AIMessage):\n",
" _m = \"AI: \" + m.content\n",
" if len(m.tool_calls) > 0:\n",
" _m += f\" Tools: {m.tool_calls}\"\n",
" return _m\n",
" elif isinstance(m, ToolMessage):\n",
" return \"Tool Result: ...\"\n",
" else:\n",
" raise ValueError\n",
"\n",
"\n",
"def _render_messages(ms):\n",
" m_string = [_render_message(m) for m in ms]\n",
" return \"\\n\".join(m_string)\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(BaseState)\n",
"\n",
"\n",
"def _agent(state: BaseState):\n",
" if len(state[\"examples\"]) > 0:\n",
" _examples = \"\\n\\n\".join(\n",
" [\n",
" f\"Example {i}: \" + _render_messages(e[\"messages\"])\n",
" for i, e in enumerate(state[\"examples\"])\n",
" ]\n",
" )\n",
" system_message = \"\"\"You are a helpful assistant. Below are some examples of interactions you had with users. \\\n",
"These were good interactions where the final result they got was the desired one. As much as possible, you should learn from these interactions and mimic them in the future. \\\n",
"Pay particularly close attention to when tools are called, and what the inputs are.!\n",
"\n",
"{examples}\n",
"\n",
"Assist the user as they require!\"\"\".format(\n",
" examples=_examples\n",
" )\n",
"\n",
" else:\n",
" system_message = \"\"\"You are a helpful assistant\"\"\"\n",
" output = model.invoke([SystemMessage(content=system_message)] + state[\"messages\"])\n",
" return {\"messages\": [output]}\n",
"\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", _agent)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")"
]
"source": ["from typing import Annotated, TypedDict\n\nfrom langchain_core.messages import (\n AIMessage,\n AnyMessage,\n HumanMessage,\n SystemMessage,\n ToolMessage,\n)\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.managed.few_shot import FewShotExamples\n\n\nclass BaseState(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n examples: Annotated[list, FewShotExamples]\n\n\ndef _render_message(m):\n if isinstance(m, HumanMessage):\n return \"Human: \" + m.content\n elif isinstance(m, AIMessage):\n _m = \"AI: \" + m.content\n if len(m.tool_calls) > 0:\n _m += f\" Tools: {m.tool_calls}\"\n return _m\n elif isinstance(m, ToolMessage):\n return \"Tool Result: ...\"\n else:\n raise ValueError\n\n\ndef _render_messages(ms):\n m_string = [_render_message(m) for m in ms]\n return \"\\n\".join(m_string)\n\n\n# Define a new graph\nworkflow = StateGraph(BaseState)\n\n\ndef _agent(state: BaseState):\n if len(state[\"examples\"]) > 0:\n _examples = \"\\n\\n\".join(\n [\n f\"Example {i}: \" + _render_messages(e[\"messages\"])\n for i, e in enumerate(state[\"examples\"])\n ]\n )\n system_message = \"\"\"You are a helpful assistant. Below are some examples of interactions you had with users. \\\nThese were good interactions where the final result they got was the desired one. As much as possible, you should learn from these interactions and mimic them in the future. \\\nPay particularly close attention to when tools are called, and what the inputs are.!\n\n{examples}\n\nAssist the user as they require!\"\"\".format(\n examples=_examples\n )\n\n else:\n system_message = \"\"\"You are a helpful assistant\"\"\"\n output = model.invoke([SystemMessage(content=system_message)] + state[\"messages\"])\n return {\"messages\": [output]}\n\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", _agent)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
},
{
"cell_type": "markdown",
@@ -363,11 +228,7 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
]
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
},
{
"cell_type": "code",
@@ -375,12 +236,7 @@
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
"metadata": {},
"outputs": [],
"source": [
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
},
{
"cell_type": "markdown",
@@ -408,11 +264,7 @@
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import Image\n",
"\n",
"Image(app.get_graph().draw_png())"
]
"source": ["from IPython.display import Image\n\nImage(app.get_graph().draw_png())"]
},
{
"cell_type": "markdown",
@@ -438,14 +290,7 @@
]
}
],
"source": [
"thread = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"for event in app.stream(\n",
" {\"messages\": [HumanMessage(content=\"what's the weather in sf?\")]}, thread\n",
"):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["thread = {\"configurable\": {\"thread_id\": \"1\"}}\nfor event in app.stream(\n {\"messages\": [HumanMessage(content=\"what's the weather in sf?\")]}, thread\n):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "code",
@@ -465,10 +310,7 @@
"output_type": "execute_result"
}
],
"source": [
"current_values = app.get_state(thread)\n",
"current_values.values"
]
"source": ["current_values = app.get_state(thread)\ncurrent_values.values"]
},
{
"cell_type": "code",
@@ -476,11 +318,7 @@
"id": "1a0cdb78-40c6-4550-8c27-8f1b02d9e678",
"metadata": {},
"outputs": [],
"source": [
"current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n",
" \"query\"\n",
"] = \"weather in San Francisco, Accuweather\""
]
"source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco, Accuweather\""]
},
{
"cell_type": "code",
@@ -500,9 +338,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.update_state(thread, current_values.values)"
]
"source": ["app.update_state(thread, current_values.values)"]
},
{
"cell_type": "code",
@@ -521,9 +357,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.get_state(thread)"
]
"source": ["app.get_state(thread)"]
},
{
"cell_type": "code",
@@ -540,11 +374,7 @@
]
}
],
"source": [
"for event in app.stream(None, thread):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["for event in app.stream(None, thread):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "code",
@@ -552,16 +382,7 @@
"id": "84748206-975e-4a33-a178-d43df683298c",
"metadata": {},
"outputs": [],
"source": [
"chkpnt_tuple = memory.get_tuple({\"configurable\": {\"thread_id\": \"1\"}})\n",
"config = chkpnt_tuple.config\n",
"checkpoint = chkpnt_tuple.checkpoint\n",
"metadata = chkpnt_tuple.metadata\n",
"\n",
"# mark as \"good\"\n",
"metadata[\"score\"] = 1\n",
"memory.put(config, checkpoint, metadata)"
]
"source": ["chkpnt_tuple = memory.get_tuple({\"configurable\": {\"thread_id\": \"1\"}})\nconfig = chkpnt_tuple.config\ncheckpoint = chkpnt_tuple.checkpoint\nmetadata = chkpnt_tuple.metadata\n\n# mark as \"good\"\nmetadata[\"score\"] = 1\nmemory.put(config, checkpoint, metadata)"]
},
{
"cell_type": "code",
@@ -569,9 +390,7 @@
"id": "ce7fa228-8c37-4001-afd4-0001b268e1db",
"metadata": {},
"outputs": [],
"source": [
"examples = list(memory.search({\"score\": 1}))"
]
"source": ["examples = list(memory.search({\"score\": 1}))"]
},
{
"cell_type": "code",
@@ -590,9 +409,7 @@
"output_type": "execute_result"
}
],
"source": [
"examples"
]
"source": ["examples"]
},
{
"cell_type": "code",
@@ -608,14 +425,7 @@
]
}
],
"source": [
"thread = {\"configurable\": {\"thread_id\": \"7\"}}\n",
"for event in app.stream(\n",
" {\"messages\": [HumanMessage(content=\"what's the weather in la?\")]}, thread\n",
"):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["thread = {\"configurable\": {\"thread_id\": \"7\"}}\nfor event in app.stream(\n {\"messages\": [HumanMessage(content=\"what's the weather in la?\")]}, thread\n):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "code",
@@ -632,11 +442,7 @@
]
}
],
"source": [
"for event in app.stream(None, thread):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["for event in app.stream(None, thread):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "code",
@@ -644,7 +450,7 @@
"id": "9ab115de-9b11-4e8b-8ace-c23e1369300b",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
+14 -158
View File
@@ -28,10 +28,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
},
{
"cell_type": "markdown",
@@ -47,18 +44,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -74,10 +60,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -103,22 +86,7 @@
"id": "5f374964",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# Add messages essentially does this with more\n",
"# robust handling\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
},
{
"cell_type": "markdown",
@@ -138,21 +106,7 @@
"id": "692cffb0",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder, but don't tell the LLM that...\n",
" return [\n",
" \"Try again in a few seconds! Checking with the weathermen... Call be again next.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\n \"Try again in a few seconds! Checking with the weathermen... Call be again next.\"\n ]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -169,11 +123,7 @@
"id": "ae7abc20",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -197,11 +147,7 @@
"id": "4ad247ff",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)"]
},
{
"cell_type": "markdown",
@@ -219,9 +165,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -256,21 +200,7 @@
"id": "e718a9c5-6596-457f-ac25-a25d8cb8c259",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state: State) -> Literal[\"__end__\", \"action\"]:\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\""
]
"source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"__end__\", \"action\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -288,19 +218,7 @@
"id": "714e4135-7cb5-4f17-b2ae-46f7e98bde61",
"metadata": {},
"outputs": [],
"source": [
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = []\n",
" for m in state[\"messages\"][::-1]:\n",
" messages.append(m)\n",
" if len(messages) >= 5:\n",
" if messages[-1].type != \"tool\":\n",
" break\n",
" response = model.invoke(messages[::-1])\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}"
]
"source": ["# Define the function that calls the model\ndef call_model(state):\n messages = []\n for m in state[\"messages\"][::-1]:\n messages.append(m)\n if len(messages) >= 5:\n if messages[-1].type != \"tool\":\n break\n response = model.invoke(messages[::-1])\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"]
},
{
"cell_type": "markdown",
@@ -318,50 +236,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -380,11 +255,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -635,22 +506,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"what is the weather in sf? Don't give up! Keep using your tools.\"\n",
" )\n",
" ]\n",
"}\n",
"for event in app.stream(inputs, stream_mode=\"values\"):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for message in event[\"messages\"]:\n",
" message.pretty_print()\n",
" print(\"\\n---\\n\")"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\n \"messages\": [\n HumanMessage(\n content=\"what is the weather in sf? Don't give up! Keep using your tools.\"\n )\n ]\n}\nfor event in app.stream(inputs, stream_mode=\"values\"):\n # stream() yields dictionaries with output keyed by node name\n for message in event[\"messages\"]:\n message.pretty_print()\n print(\"\\n---\\n\")"]
},
{
"cell_type": "code",
@@ -658,7 +514,7 @@
"id": "08ae8246-11d5-40e1-8567-361e5bef8917",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+4 -4
View File
@@ -98,7 +98,7 @@
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.graph import MessagesState, StateGraph\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
@@ -146,7 +146,7 @@
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
@@ -229,7 +229,7 @@
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"from langgraph.graph import MessagesState, StateGraph\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")\n",
@@ -283,7 +283,7 @@
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
+2 -105
View File
@@ -34,110 +34,7 @@
]
}
],
"source": [
"import operator\n",
"from typing import Annotated, TypedDict\n",
"\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.constants import Send\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Model and prompts\n",
"# Define model and prompts we will use\n",
"subjects_prompt = \"\"\"Generate a comma separated list of between 2 and 5 {topic}.\"\"\"\n",
"joke_prompt = \"\"\"Generate a joke about {subject}\"\"\"\n",
"best_joke_prompt = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one.\n",
"\n",
"{jokes}\"\"\"\n",
"\n",
"\n",
"class Subjects(BaseModel):\n",
" subjects: list[str]\n",
"\n",
"\n",
"class Joke(BaseModel):\n",
" joke: str\n",
"\n",
"\n",
"class BestJoke(BaseModel):\n",
" id: int\n",
"\n",
"\n",
"model = ChatOpenAI()\n",
"\n",
"# Graph components: define the components that will make up the graph\n",
"\n",
"\n",
"# This will be the overall state of the main graph.\n",
"# It will contain a topic (which we expect the user to provide)\n",
"# and then will generate a list of subjects, and then a joke for\n",
"# each subject\n",
"class OverallState(TypedDict):\n",
" topic: str\n",
" subjects: list\n",
" # Notice here we use the operator.add\n",
" # This is because we want combine all the jokes we generate\n",
" # from individual nodes back into one list - this is essentially\n",
" # the \"reduce\" part\n",
" jokes: Annotated[list, operator.add]\n",
" best_selected_joke: str\n",
"\n",
"\n",
"# This will be the state of the node that we will \"map\" all\n",
"# subjects to in order to generate a joke\n",
"class JokeState(TypedDict):\n",
" subject: str\n",
"\n",
"\n",
"# This is the function we will use to generate the subjects of the jokes\n",
"def generate_topics(state: OverallState):\n",
" prompt = subjects_prompt.format(topic=state[\"topic\"])\n",
" response = model.with_structured_output(Subjects).invoke(prompt)\n",
" return {\"subjects\": response.subjects}\n",
"\n",
"\n",
"# Here we generate a joke, given a subject\n",
"def generate_joke(state: JokeState):\n",
" prompt = joke_prompt.format(subject=state[\"subject\"])\n",
" response = model.with_structured_output(Joke).invoke(prompt)\n",
" return {\"jokes\": [response.joke]}\n",
"\n",
"\n",
"# Here we define the logic to map out over the generated subjects\n",
"# We will use this an edge in the graph\n",
"def continue_to_jokes(state: OverallState):\n",
" # We will return a list of `Send` objects\n",
" # Each `Send` object consists of the name of a node in the graph\n",
" # as well as the state to send to that node\n",
" return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]\n",
"\n",
"\n",
"# Here we will judge the best joke\n",
"def best_joke(state: OverallState):\n",
" jokes = \"\\n\\n\".format()\n",
" prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n",
" response = model.with_structured_output(BestJoke).invoke(prompt)\n",
" return {\"best_selected_joke\": state[\"jokes\"][response.id]}\n",
"\n",
"\n",
"# Construct the graph: here we put everything together to construct our graph\n",
"graph = StateGraph(OverallState)\n",
"graph.add_node(\"generate_topics\", generate_topics)\n",
"graph.add_node(\"generate_joke\", generate_joke)\n",
"graph.add_node(\"best_joke\", best_joke)\n",
"graph.set_entry_point(\"generate_topics\")\n",
"graph.add_conditional_edges(\"generate_topics\", continue_to_jokes)\n",
"graph.add_edge(\"generate_joke\", \"best_joke\")\n",
"graph.add_edge(\"best_joke\", END)\n",
"app = graph.compile()\n",
"\n",
"\n",
"# Call the graph: here we call it to generate a list of jokes\n",
"for s in app.stream({\"topic\": \"animals\"}):\n",
" print(s)"
]
"source": ["import operator\nfrom typing import Annotated, TypedDict\n\nfrom langchain_core.pydantic_v1 import BaseModel\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.constants import Send\nfrom langgraph.graph import END, StateGraph, START\n\n# Model and prompts\n# Define model and prompts we will use\nsubjects_prompt = \"\"\"Generate a comma separated list of between 2 and 5 {topic}.\"\"\"\njoke_prompt = \"\"\"Generate a joke about {subject}\"\"\"\nbest_joke_prompt = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one.\n\n{jokes}\"\"\"\n\n\nclass Subjects(BaseModel):\n subjects: list[str]\n\n\nclass Joke(BaseModel):\n joke: str\n\n\nclass BestJoke(BaseModel):\n id: int\n\n\nmodel = ChatOpenAI()\n\n# Graph components: define the components that will make up the graph\n\n\n# This will be the overall state of the main graph.\n# It will contain a topic (which we expect the user to provide)\n# and then will generate a list of subjects, and then a joke for\n# each subject\nclass OverallState(TypedDict):\n topic: str\n subjects: list\n # Notice here we use the operator.add\n # This is because we want combine all the jokes we generate\n # from individual nodes back into one list - this is essentially\n # the \"reduce\" part\n jokes: Annotated[list, operator.add]\n best_selected_joke: str\n\n\n# This will be the state of the node that we will \"map\" all\n# subjects to in order to generate a joke\nclass JokeState(TypedDict):\n subject: str\n\n\n# This is the function we will use to generate the subjects of the jokes\ndef generate_topics(state: OverallState):\n prompt = subjects_prompt.format(topic=state[\"topic\"])\n response = model.with_structured_output(Subjects).invoke(prompt)\n return {\"subjects\": response.subjects}\n\n\n# Here we generate a joke, given a subject\ndef generate_joke(state: JokeState):\n prompt = joke_prompt.format(subject=state[\"subject\"])\n response = model.with_structured_output(Joke).invoke(prompt)\n return {\"jokes\": [response.joke]}\n\n\n# Here we define the logic to map out over the generated subjects\n# We will use this an edge in the graph\ndef continue_to_jokes(state: OverallState):\n # We will return a list of `Send` objects\n # Each `Send` object consists of the name of a node in the graph\n # as well as the state to send to that node\n return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]\n\n\n# Here we will judge the best joke\ndef best_joke(state: OverallState):\n jokes = \"\\n\\n\".format()\n prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n response = model.with_structured_output(BestJoke).invoke(prompt)\n return {\"best_selected_joke\": state[\"jokes\"][response.id]}\n\n\n# Construct the graph: here we put everything together to construct our graph\ngraph = StateGraph(OverallState)\ngraph.add_node(\"generate_topics\", generate_topics)\ngraph.add_node(\"generate_joke\", generate_joke)\ngraph.add_node(\"best_joke\", best_joke)\ngraph.add_edge(START, \"generate_topics\")\ngraph.add_conditional_edges(\"generate_topics\", continue_to_jokes)\ngraph.add_edge(\"generate_joke\", \"best_joke\")\ngraph.add_edge(\"best_joke\", END)\napp = graph.compile()\n\n\n# Call the graph: here we call it to generate a list of jokes\nfor s in app.stream({\"topic\": \"animals\"}):\n print(s)"]
},
{
"cell_type": "code",
@@ -145,7 +42,7 @@
"id": "37ed1f71-63db-416f-b715-4617b33d4b7f",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+11 -184
View File
@@ -26,10 +26,7 @@
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"]
},
{
"cell_type": "code",
@@ -37,24 +34,7 @@
"id": "30c2f3de-c730-4aec-85a6-af2c2f058803",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")\n",
"\n",
"# Optional, add tracing in LangSmith\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""]
},
{
"cell_type": "markdown",
@@ -72,17 +52,7 @@
"id": "f04c6778-403b-4b49-9b93-678e910d5cec",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_experimental.tools import PythonREPLTool\n",
"\n",
"tavily_tool = TavilySearchResults(max_results=5)\n",
"\n",
"# This executes code locally, which can be unsafe\n",
"python_repl_tool = PythonREPLTool()"
]
"source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_experimental.tools import PythonREPLTool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# This executes code locally, which can be unsafe\npython_repl_tool = PythonREPLTool()"]
},
{
"cell_type": "markdown",
@@ -100,28 +70,7 @@
"id": "c4823dd9-26bd-4e1a-8117-b97b2860211a",
"metadata": {},
"outputs": [],
"source": [
"from langchain.agents import AgentExecutor, create_openai_tools_agent\n",
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"\n",
"def create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n",
" # Each worker node will be given a name and some tools.\n",
" prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" system_prompt,\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
" ]\n",
" )\n",
" agent = create_openai_tools_agent(llm, tools, prompt)\n",
" executor = AgentExecutor(agent=agent, tools=tools)\n",
" return executor"
]
"source": ["from langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\n\ndef create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n # Each worker node will be given a name and some tools.\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_tools_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor"]
},
{
"cell_type": "markdown",
@@ -137,11 +86,7 @@
"id": "80862241-a1a7-4726-bce5-f867b233832e",
"metadata": {},
"outputs": [],
"source": [
"def agent_node(state, agent, name):\n",
" result = agent.invoke(state)\n",
" return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}"
]
"source": ["def agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}"]
},
{
"cell_type": "markdown",
@@ -159,59 +104,7 @@
"id": "311f0a58-b425-4496-adac-dc4cd8ffb912",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"\n",
"members = [\"Researcher\", \"Coder\"]\n",
"system_prompt = (\n",
" \"You are a supervisor tasked with managing a conversation between the\"\n",
" \" following workers: {members}. Given the following user request,\"\n",
" \" respond with the worker to act next. Each worker will perform a\"\n",
" \" task and respond with their results and status. When finished,\"\n",
" \" respond with FINISH.\"\n",
")\n",
"# Our team supervisor is an LLM node. It just picks the next agent to process\n",
"# and decides when the work is completed\n",
"options = [\"FINISH\"] + members\n",
"# Using openai function calling can make output parsing easier for us\n",
"function_def = {\n",
" \"name\": \"route\",\n",
" \"description\": \"Select the next role.\",\n",
" \"parameters\": {\n",
" \"title\": \"routeSchema\",\n",
" \"type\": \"object\",\n",
" \"properties\": {\n",
" \"next\": {\n",
" \"title\": \"Next\",\n",
" \"anyOf\": [\n",
" {\"enum\": options},\n",
" ],\n",
" }\n",
" },\n",
" \"required\": [\"next\"],\n",
" },\n",
"}\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" (\n",
" \"system\",\n",
" \"Given the conversation above, who should act next?\"\n",
" \" Or should we FINISH? Select one of: {options}\",\n",
" ),\n",
" ]\n",
").partial(options=str(options), members=\", \".join(members))\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
"\n",
"supervisor_chain = (\n",
" prompt\n",
" | llm.bind_functions(functions=[function_def], function_call=\"route\")\n",
" | JsonOutputFunctionsParser()\n",
")"
]
"source": ["from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nmembers = [\"Researcher\", \"Coder\"]\nsystem_prompt = (\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\"\n)\n# Our team supervisor is an LLM node. It just picks the next agent to process\n# and decides when the work is completed\noptions = [\"FINISH\"] + members\n# Using openai function calling can make output parsing easier for us\nfunction_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n }\n },\n \"required\": [\"next\"],\n },\n}\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n).partial(options=str(options), members=\", \".join(members))\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_chain = (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n)"]
},
{
"cell_type": "markdown",
@@ -229,41 +122,7 @@
"id": "6a430af7-8fce-4e66-ba9e-d940c1bc48e8",
"metadata": {},
"outputs": [],
"source": [
"import functools\n",
"import operator\n",
"from typing import Sequence, TypedDict\n",
"\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"\n",
"# The agent state is the input to each node in the graph\n",
"class AgentState(TypedDict):\n",
" # The annotation tells the graph that new messages will always\n",
" # be added to the current states\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]\n",
" # The 'next' field indicates where to route to next\n",
" next: str\n",
"\n",
"\n",
"research_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\n",
"research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n",
"\n",
"# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\n",
"code_agent = create_agent(\n",
" llm,\n",
" [python_repl_tool],\n",
" \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n",
")\n",
"code_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n",
"\n",
"workflow = StateGraph(AgentState)\n",
"workflow.add_node(\"Researcher\", research_node)\n",
"workflow.add_node(\"Coder\", code_node)\n",
"workflow.add_node(\"supervisor\", supervisor_chain)"
]
"source": ["import functools\nimport operator\nfrom typing import Sequence, TypedDict\n\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\n# The agent state is the input to each node in the graph\nclass AgentState(TypedDict):\n # The annotation tells the graph that new messages will always\n # be added to the current states\n messages: Annotated[Sequence[BaseMessage], operator.add]\n # The 'next' field indicates where to route to next\n next: str\n\n\nresearch_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\ncode_agent = create_agent(\n llm,\n [python_repl_tool],\n \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n)\ncode_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"Coder\", code_node)\nworkflow.add_node(\"supervisor\", supervisor_chain)"]
},
{
"cell_type": "markdown",
@@ -279,20 +138,7 @@
"id": "14778e86-077b-4e6a-893c-400e59b0cdbf",
"metadata": {},
"outputs": [],
"source": [
"for member in members:\n",
" # We want our workers to ALWAYS \"report back\" to the supervisor when done\n",
" workflow.add_edge(member, \"supervisor\")\n",
"# The supervisor populates the \"next\" field in the graph state\n",
"# which routes to a node or finishes\n",
"conditional_map = {k: k for k in members}\n",
"conditional_map[\"FINISH\"] = END\n",
"workflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n",
"# Finally, add entrypoint\n",
"workflow.set_entry_point(\"supervisor\")\n",
"\n",
"graph = workflow.compile()"
]
"source": ["for member in members:\n # We want our workers to ALWAYS \"report back\" to the supervisor when done\n workflow.add_edge(member, \"supervisor\")\n# The supervisor populates the \"next\" field in the graph state\n# which routes to a node or finishes\nconditional_map = {k: k for k in members}\nconditional_map[\"FINISH\"] = END\nworkflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n# Finally, add entrypoint\nworkflow.add_edge(START, \"supervisor\")\n\ngraph = workflow.compile()"]
},
{
"cell_type": "markdown",
@@ -336,18 +182,7 @@
]
}
],
"source": [
"for s in graph.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(content=\"Code hello world and print it to the terminal\")\n",
" ]\n",
" }\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"----\")"
]
"source": ["for s in graph.stream(\n {\n \"messages\": [\n HumanMessage(content=\"Code hello world and print it to the terminal\")\n ]\n }\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -368,15 +203,7 @@
]
}
],
"source": [
"for s in graph.stream(\n",
" {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n",
" {\"recursion_limit\": 100},\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"----\")"
]
"source": ["for s in graph.stream(\n {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -384,7 +211,7 @@
"id": "1d363d2c-e0da-4cce-ba47-ad2aa9df0fef",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -40,10 +40,7 @@
}
},
"outputs": [],
"source": [
"# %%capture --no-stderr\n",
"# %pip install -U langgraph langchain langchain_openai langchain_experimental"
]
"source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai langchain_experimental"]
},
{
"cell_type": "code",
@@ -56,25 +53,7 @@
}
},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")\n",
"\n",
"# Optional, add tracing in LangSmith.\n",
"# This will help you visualize and debug the control flow\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""]
},
{
"cell_type": "markdown",
@@ -103,28 +82,7 @@
}
},
"outputs": [],
"source": [
"from typing import Annotated, List\n",
"\n",
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.tools import tool\n",
"\n",
"tavily_tool = TavilySearchResults(max_results=5)\n",
"\n",
"\n",
"@tool\n",
"def scrape_webpages(urls: List[str]) -> str:\n",
" \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n",
" loader = WebBaseLoader(urls)\n",
" docs = loader.load()\n",
" return \"\\n\\n\".join(\n",
" [\n",
" f'<Document name=\"{doc.metadata.get(\"title\", \"\")}\">\\n{doc.page_content}\\n</Document>'\n",
" for doc in docs\n",
" ]\n",
" )"
]
"source": ["from typing import Annotated, List\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n\n@tool\ndef scrape_webpages(urls: List[str]) -> str:\n \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n loader = WebBaseLoader(urls)\n docs = loader.load()\n return \"\\n\\n\".join(\n [\n f'<Document name=\"{doc.metadata.get(\"title\", \"\")}\">\\n{doc.page_content}\\n</Document>'\n for doc in docs\n ]\n )"]
},
{
"cell_type": "markdown",
@@ -150,99 +108,7 @@
}
},
"outputs": [],
"source": [
"from pathlib import Path\n",
"from tempfile import TemporaryDirectory\n",
"from typing import Dict, Optional\n",
"\n",
"from langchain_experimental.utilities import PythonREPL\n",
"from typing_extensions import TypedDict\n",
"\n",
"_TEMP_DIRECTORY = TemporaryDirectory()\n",
"WORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n",
"\n",
"\n",
"@tool\n",
"def create_outline(\n",
" points: Annotated[List[str], \"List of main points or sections.\"],\n",
" file_name: Annotated[str, \"File path to save the outline.\"],\n",
") -> Annotated[str, \"Path of the saved outline file.\"]:\n",
" \"\"\"Create and save an outline.\"\"\"\n",
" with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n",
" for i, point in enumerate(points):\n",
" file.write(f\"{i + 1}. {point}\\n\")\n",
" return f\"Outline saved to {file_name}\"\n",
"\n",
"\n",
"@tool\n",
"def read_document(\n",
" file_name: Annotated[str, \"File path to save the document.\"],\n",
" start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n",
" end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n",
") -> str:\n",
" \"\"\"Read the specified document.\"\"\"\n",
" with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n",
" lines = file.readlines()\n",
" if start is not None:\n",
" start = 0\n",
" return \"\\n\".join(lines[start:end])\n",
"\n",
"\n",
"@tool\n",
"def write_document(\n",
" content: Annotated[str, \"Text content to be written into the document.\"],\n",
" file_name: Annotated[str, \"File path to save the document.\"],\n",
") -> Annotated[str, \"Path of the saved document file.\"]:\n",
" \"\"\"Create and save a text document.\"\"\"\n",
" with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n",
" file.write(content)\n",
" return f\"Document saved to {file_name}\"\n",
"\n",
"\n",
"@tool\n",
"def edit_document(\n",
" file_name: Annotated[str, \"Path of the document to be edited.\"],\n",
" inserts: Annotated[\n",
" Dict[int, str],\n",
" \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n",
" ],\n",
") -> Annotated[str, \"Path of the edited document file.\"]:\n",
" \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n",
"\n",
" with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n",
" lines = file.readlines()\n",
"\n",
" sorted_inserts = sorted(inserts.items())\n",
"\n",
" for line_number, text in sorted_inserts:\n",
" if 1 <= line_number <= len(lines) + 1:\n",
" lines.insert(line_number - 1, text + \"\\n\")\n",
" else:\n",
" return f\"Error: Line number {line_number} is out of range.\"\n",
"\n",
" with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n",
" file.writelines(lines)\n",
"\n",
" return f\"Document edited and saved to {file_name}\"\n",
"\n",
"\n",
"# Warning: This executes code locally, which can be unsafe when not sandboxed\n",
"\n",
"repl = PythonREPL()\n",
"\n",
"\n",
"@tool\n",
"def python_repl(\n",
" code: Annotated[str, \"The python code to execute to generate your chart.\"],\n",
"):\n",
" \"\"\"Use this to execute python code. If you want to see the output of a value,\n",
" you should print it out with `print(...)`. This is visible to the user.\"\"\"\n",
" try:\n",
" result = repl.run(code)\n",
" except BaseException as e:\n",
" return f\"Failed to execute. Error: {repr(e)}\"\n",
" return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\""
]
"source": ["from pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Dict, Optional\n\nfrom langchain_experimental.utilities import PythonREPL\nfrom typing_extensions import TypedDict\n\n_TEMP_DIRECTORY = TemporaryDirectory()\nWORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n\n\n@tool\ndef create_outline(\n points: Annotated[List[str], \"List of main points or sections.\"],\n file_name: Annotated[str, \"File path to save the outline.\"],\n) -> Annotated[str, \"Path of the saved outline file.\"]:\n \"\"\"Create and save an outline.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n for i, point in enumerate(points):\n file.write(f\"{i + 1}. {point}\\n\")\n return f\"Outline saved to {file_name}\"\n\n\n@tool\ndef read_document(\n file_name: Annotated[str, \"File path to save the document.\"],\n start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n) -> str:\n \"\"\"Read the specified document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n if start is not None:\n start = 0\n return \"\\n\".join(lines[start:end])\n\n\n@tool\ndef write_document(\n content: Annotated[str, \"Text content to be written into the document.\"],\n file_name: Annotated[str, \"File path to save the document.\"],\n) -> Annotated[str, \"Path of the saved document file.\"]:\n \"\"\"Create and save a text document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.write(content)\n return f\"Document saved to {file_name}\"\n\n\n@tool\ndef edit_document(\n file_name: Annotated[str, \"Path of the document to be edited.\"],\n inserts: Annotated[\n Dict[int, str],\n \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n ],\n) -> Annotated[str, \"Path of the edited document file.\"]:\n \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n\n sorted_inserts = sorted(inserts.items())\n\n for line_number, text in sorted_inserts:\n if 1 <= line_number <= len(lines) + 1:\n lines.insert(line_number - 1, text + \"\\n\")\n else:\n return f\"Error: Line number {line_number} is out of range.\"\n\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.writelines(lines)\n\n return f\"Document edited and saved to {file_name}\"\n\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\""]
},
{
"cell_type": "markdown",
@@ -270,84 +136,7 @@
}
},
"outputs": [],
"source": [
"from typing import List, Optional\n",
"\n",
"from langchain.agents import AgentExecutor, create_openai_functions_agent\n",
"from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"\n",
"def create_agent(\n",
" llm: ChatOpenAI,\n",
" tools: list,\n",
" system_prompt: str,\n",
") -> str:\n",
" \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n",
" system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n",
" \" Do not ask for clarification.\"\n",
" \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n",
" \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n",
" prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" system_prompt,\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
" ]\n",
" )\n",
" agent = create_openai_functions_agent(llm, tools, prompt)\n",
" executor = AgentExecutor(agent=agent, tools=tools)\n",
" return executor\n",
"\n",
"\n",
"def agent_node(state, agent, name):\n",
" result = agent.invoke(state)\n",
" return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n",
"\n",
"\n",
"def create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n",
" \"\"\"An LLM-based router.\"\"\"\n",
" options = [\"FINISH\"] + members\n",
" function_def = {\n",
" \"name\": \"route\",\n",
" \"description\": \"Select the next role.\",\n",
" \"parameters\": {\n",
" \"title\": \"routeSchema\",\n",
" \"type\": \"object\",\n",
" \"properties\": {\n",
" \"next\": {\n",
" \"title\": \"Next\",\n",
" \"anyOf\": [\n",
" {\"enum\": options},\n",
" ],\n",
" },\n",
" },\n",
" \"required\": [\"next\"],\n",
" },\n",
" }\n",
" prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system_prompt),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" (\n",
" \"system\",\n",
" \"Given the conversation above, who should act next?\"\n",
" \" Or should we FINISH? Select one of: {options}\",\n",
" ),\n",
" ]\n",
" ).partial(options=str(options), team_members=\", \".join(members))\n",
" return (\n",
" prompt\n",
" | llm.bind_functions(functions=[function_def], function_call=\"route\")\n",
" | JsonOutputFunctionsParser()\n",
" )"
]
"source": ["from typing import List, Optional\n\nfrom langchain.agents import AgentExecutor, create_openai_functions_agent\nfrom langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(\n llm: ChatOpenAI,\n tools: list,\n system_prompt: str,\n) -> str:\n \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n \" Do not ask for clarification.\"\n \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_functions_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor\n\n\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n\n\ndef create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n \"\"\"An LLM-based router.\"\"\"\n options = [\"FINISH\"] + members\n function_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n },\n },\n \"required\": [\"next\"],\n },\n }\n prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n ).partial(options=str(options), team_members=\", \".join(members))\n return (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n )"]
},
{
"cell_type": "markdown",
@@ -374,52 +163,7 @@
}
},
"outputs": [],
"source": [
"import functools\n",
"import operator\n",
"\n",
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"from langchain_openai.chat_models import ChatOpenAI\n",
"\n",
"\n",
"# ResearchTeam graph state\n",
"class ResearchTeamState(TypedDict):\n",
" # A message is added after each team member finishes\n",
" messages: Annotated[List[BaseMessage], operator.add]\n",
" # The team members are tracked so they are aware of\n",
" # the others' skill-sets\n",
" team_members: List[str]\n",
" # Used to route work. The supervisor calls a function\n",
" # that will update this every time it makes a decision\n",
" next: str\n",
"\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
"\n",
"search_agent = create_agent(\n",
" llm,\n",
" [tavily_tool],\n",
" \"You are a research assistant who can search for up-to-date info using the tavily search engine.\",\n",
")\n",
"search_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n",
"\n",
"research_agent = create_agent(\n",
" llm,\n",
" [scrape_webpages],\n",
" \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\",\n",
")\n",
"research_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n",
"\n",
"supervisor_agent = create_team_supervisor(\n",
" llm,\n",
" \"You are a supervisor tasked with managing a conversation between the\"\n",
" \" following workers: Search, WebScraper. Given the following user request,\"\n",
" \" respond with the worker to act next. Each worker will perform a\"\n",
" \" task and respond with their results and status. When finished,\"\n",
" \" respond with FINISH.\",\n",
" [\"Search\", \"WebScraper\"],\n",
")"
]
"source": ["import functools\nimport operator\n\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\n\n# ResearchTeam graph state\nclass ResearchTeamState(TypedDict):\n # A message is added after each team member finishes\n messages: Annotated[List[BaseMessage], operator.add]\n # The team members are tracked so they are aware of\n # the others' skill-sets\n team_members: List[str]\n # Used to route work. The supervisor calls a function\n # that will update this every time it makes a decision\n next: str\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsearch_agent = create_agent(\n llm,\n [tavily_tool],\n \"You are a research assistant who can search for up-to-date info using the tavily search engine.\",\n)\nsearch_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n\nresearch_agent = create_agent(\n llm,\n [scrape_webpages],\n \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n\nsupervisor_agent = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: Search, WebScraper. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"Search\", \"WebScraper\"],\n)"]
},
{
"cell_type": "markdown",
@@ -440,38 +184,7 @@
}
},
"outputs": [],
"source": [
"research_graph = StateGraph(ResearchTeamState)\n",
"research_graph.add_node(\"Search\", search_node)\n",
"research_graph.add_node(\"WebScraper\", research_node)\n",
"research_graph.add_node(\"supervisor\", supervisor_agent)\n",
"\n",
"# Define the control flow\n",
"research_graph.add_edge(\"Search\", \"supervisor\")\n",
"research_graph.add_edge(\"WebScraper\", \"supervisor\")\n",
"research_graph.add_conditional_edges(\n",
" \"supervisor\",\n",
" lambda x: x[\"next\"],\n",
" {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n",
")\n",
"\n",
"\n",
"research_graph.set_entry_point(\"supervisor\")\n",
"chain = research_graph.compile()\n",
"\n",
"\n",
"# The following functions interoperate between the top level graph state\n",
"# and the state of the research sub-graph\n",
"# this makes it so that the states of each graph don't get intermixed\n",
"def enter_chain(message: str):\n",
" results = {\n",
" \"messages\": [HumanMessage(content=message)],\n",
" }\n",
" return results\n",
"\n",
"\n",
"research_chain = enter_chain | chain"
]
"source": ["research_graph = StateGraph(ResearchTeamState)\nresearch_graph.add_node(\"Search\", search_node)\nresearch_graph.add_node(\"WebScraper\", research_node)\nresearch_graph.add_node(\"supervisor\", supervisor_agent)\n\n# Define the control flow\nresearch_graph.add_edge(\"Search\", \"supervisor\")\nresearch_graph.add_edge(\"WebScraper\", \"supervisor\")\nresearch_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n)\n\n\nresearch_graph.add_edge(START, \"supervisor\")\nchain = research_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str):\n results = {\n \"messages\": [HumanMessage(content=message)],\n }\n return results\n\n\nresearch_chain = enter_chain | chain"]
},
{
"cell_type": "code",
@@ -495,11 +208,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(chain.get_graph(xray=True).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph(xray=True).draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -520,14 +229,7 @@
}
},
"outputs": [],
"source": [
"for s in research_chain.stream(\n",
" \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"---\")"
]
"source": ["for s in research_chain.stream(\n \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"]
},
{
"cell_type": "markdown",
@@ -552,92 +254,7 @@
}
},
"outputs": [],
"source": [
"import operator\n",
"from pathlib import Path\n",
"\n",
"\n",
"# Document writing team graph state\n",
"class DocWritingState(TypedDict):\n",
" # This tracks the team's conversation internally\n",
" messages: Annotated[List[BaseMessage], operator.add]\n",
" # This provides each worker with context on the others' skill sets\n",
" team_members: str\n",
" # This is how the supervisor tells langgraph who to work next\n",
" next: str\n",
" # This tracks the shared directory state\n",
" current_files: str\n",
"\n",
"\n",
"# This will be run before each worker agent begins work\n",
"# It makes it so they are more aware of the current state\n",
"# of the working directory.\n",
"def prelude(state):\n",
" written_files = []\n",
" if not WORKING_DIRECTORY.exists():\n",
" WORKING_DIRECTORY.mkdir()\n",
" try:\n",
" written_files = [\n",
" f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n",
" ]\n",
" except Exception:\n",
" pass\n",
" if not written_files:\n",
" return {**state, \"current_files\": \"No files written.\"}\n",
" return {\n",
" **state,\n",
" \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n",
" + \"\\n\".join([f\" - {f}\" for f in written_files]),\n",
" }\n",
"\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
"\n",
"doc_writer_agent = create_agent(\n",
" llm,\n",
" [write_document, edit_document, read_document],\n",
" \"You are an expert writing a research document.\\n\"\n",
" # The {current_files} value is populated automatically by the graph state\n",
" \"Below are files currently in your directory:\\n{current_files}\",\n",
")\n",
"# Injects current directory working state before each call\n",
"context_aware_doc_writer_agent = prelude | doc_writer_agent\n",
"doc_writing_node = functools.partial(\n",
" agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n",
")\n",
"\n",
"note_taking_agent = create_agent(\n",
" llm,\n",
" [create_outline, read_document],\n",
" \"You are an expert senior researcher tasked with writing a paper outline and\"\n",
" \" taking notes to craft a perfect paper.{current_files}\",\n",
")\n",
"context_aware_note_taking_agent = prelude | note_taking_agent\n",
"note_taking_node = functools.partial(\n",
" agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n",
")\n",
"\n",
"chart_generating_agent = create_agent(\n",
" llm,\n",
" [read_document, python_repl],\n",
" \"You are a data viz expert tasked with generating charts for a research project.\"\n",
" \"{current_files}\",\n",
")\n",
"context_aware_chart_generating_agent = prelude | chart_generating_agent\n",
"chart_generating_node = functools.partial(\n",
" agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n",
")\n",
"\n",
"doc_writing_supervisor = create_team_supervisor(\n",
" llm,\n",
" \"You are a supervisor tasked with managing a conversation between the\"\n",
" \" following workers: {team_members}. Given the following user request,\"\n",
" \" respond with the worker to act next. Each worker will perform a\"\n",
" \" task and respond with their results and status. When finished,\"\n",
" \" respond with FINISH.\",\n",
" [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n",
")"
]
"source": ["import operator\nfrom pathlib import Path\n\n\n# Document writing team graph state\nclass DocWritingState(TypedDict):\n # This tracks the team's conversation internally\n messages: Annotated[List[BaseMessage], operator.add]\n # This provides each worker with context on the others' skill sets\n team_members: str\n # This is how the supervisor tells langgraph who to work next\n next: str\n # This tracks the shared directory state\n current_files: str\n\n\n# This will be run before each worker agent begins work\n# It makes it so they are more aware of the current state\n# of the working directory.\ndef prelude(state):\n written_files = []\n if not WORKING_DIRECTORY.exists():\n WORKING_DIRECTORY.mkdir()\n try:\n written_files = [\n f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n ]\n except Exception:\n pass\n if not written_files:\n return {**state, \"current_files\": \"No files written.\"}\n return {\n **state,\n \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n + \"\\n\".join([f\" - {f}\" for f in written_files]),\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\ndoc_writer_agent = create_agent(\n llm,\n [write_document, edit_document, read_document],\n \"You are an expert writing a research document.\\n\"\n # The {current_files} value is populated automatically by the graph state\n \"Below are files currently in your directory:\\n{current_files}\",\n)\n# Injects current directory working state before each call\ncontext_aware_doc_writer_agent = prelude | doc_writer_agent\ndoc_writing_node = functools.partial(\n agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n)\n\nnote_taking_agent = create_agent(\n llm,\n [create_outline, read_document],\n \"You are an expert senior researcher tasked with writing a paper outline and\"\n \" taking notes to craft a perfect paper.{current_files}\",\n)\ncontext_aware_note_taking_agent = prelude | note_taking_agent\nnote_taking_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n)\n\nchart_generating_agent = create_agent(\n llm,\n [read_document, python_repl],\n \"You are a data viz expert tasked with generating charts for a research project.\"\n \"{current_files}\",\n)\ncontext_aware_chart_generating_agent = prelude | chart_generating_agent\nchart_generating_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n)\n\ndoc_writing_supervisor = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n)"]
},
{
"cell_type": "markdown",
@@ -658,53 +275,7 @@
}
},
"outputs": [],
"source": [
"# Create the graph here:\n",
"# Note that we have unrolled the loop for the sake of this doc\n",
"authoring_graph = StateGraph(DocWritingState)\n",
"authoring_graph.add_node(\"DocWriter\", doc_writing_node)\n",
"authoring_graph.add_node(\"NoteTaker\", note_taking_node)\n",
"authoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\n",
"authoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n",
"\n",
"# Add the edges that always occur\n",
"authoring_graph.add_edge(\"DocWriter\", \"supervisor\")\n",
"authoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\n",
"authoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n",
"\n",
"# Add the edges where routing applies\n",
"authoring_graph.add_conditional_edges(\n",
" \"supervisor\",\n",
" lambda x: x[\"next\"],\n",
" {\n",
" \"DocWriter\": \"DocWriter\",\n",
" \"NoteTaker\": \"NoteTaker\",\n",
" \"ChartGenerator\": \"ChartGenerator\",\n",
" \"FINISH\": END,\n",
" },\n",
")\n",
"\n",
"authoring_graph.set_entry_point(\"supervisor\")\n",
"chain = authoring_graph.compile()\n",
"\n",
"\n",
"# The following functions interoperate between the top level graph state\n",
"# and the state of the research sub-graph\n",
"# this makes it so that the states of each graph don't get intermixed\n",
"def enter_chain(message: str, members: List[str]):\n",
" results = {\n",
" \"messages\": [HumanMessage(content=message)],\n",
" \"team_members\": \", \".join(members),\n",
" }\n",
" return results\n",
"\n",
"\n",
"# We reuse the enter/exit functions to wrap the graph\n",
"authoring_chain = (\n",
" functools.partial(enter_chain, members=authoring_graph.nodes)\n",
" | authoring_graph.compile()\n",
")"
]
"source": ["# Create the graph here:\n# Note that we have unrolled the loop for the sake of this doc\nauthoring_graph = StateGraph(DocWritingState)\nauthoring_graph.add_node(\"DocWriter\", doc_writing_node)\nauthoring_graph.add_node(\"NoteTaker\", note_taking_node)\nauthoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\nauthoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n\n# Add the edges that always occur\nauthoring_graph.add_edge(\"DocWriter\", \"supervisor\")\nauthoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\nauthoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n\n# Add the edges where routing applies\nauthoring_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"DocWriter\": \"DocWriter\",\n \"NoteTaker\": \"NoteTaker\",\n \"ChartGenerator\": \"ChartGenerator\",\n \"FINISH\": END,\n },\n)\n\nauthoring_graph.add_edge(START, \"supervisor\")\nchain = authoring_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str, members: List[str]):\n results = {\n \"messages\": [HumanMessage(content=message)],\n \"team_members\": \", \".join(members),\n }\n return results\n\n\n# We reuse the enter/exit functions to wrap the graph\nauthoring_chain = (\n functools.partial(enter_chain, members=authoring_graph.nodes)\n | authoring_graph.compile()\n)"]
},
{
"cell_type": "code",
@@ -728,11 +299,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(chain.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -745,15 +312,7 @@
}
},
"outputs": [],
"source": [
"for s in authoring_chain.stream(\n",
" \"Write an outline for poem and then write the poem to disk.\",\n",
" {\"recursion_limit\": 100},\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"---\")"
]
"source": ["for s in authoring_chain.stream(\n \"Write an outline for poem and then write the poem to disk.\",\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"]
},
{
"cell_type": "markdown",
@@ -778,22 +337,7 @@
}
},
"outputs": [],
"source": [
"from langchain_core.messages import BaseMessage\n",
"from langchain_openai.chat_models import ChatOpenAI\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
"\n",
"supervisor_node = create_team_supervisor(\n",
" llm,\n",
" \"You are a supervisor tasked with managing a conversation between the\"\n",
" \" following teams: {team_members}. Given the following user request,\"\n",
" \" respond with the worker to act next. Each worker will perform a\"\n",
" \" task and respond with their results and status. When finished,\"\n",
" \" respond with FINISH.\",\n",
" [\"ResearchTeam\", \"PaperWritingTeam\"],\n",
")"
]
"source": ["from langchain_core.messages import BaseMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_node = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following teams: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"ResearchTeam\", \"PaperWritingTeam\"],\n)"]
},
{
"cell_type": "code",
@@ -806,46 +350,7 @@
}
},
"outputs": [],
"source": [
"# Top-level graph state\n",
"class State(TypedDict):\n",
" messages: Annotated[List[BaseMessage], operator.add]\n",
" next: str\n",
"\n",
"\n",
"def get_last_message(state: State) -> str:\n",
" return state[\"messages\"][-1].content\n",
"\n",
"\n",
"def join_graph(response: dict):\n",
" return {\"messages\": [response[\"messages\"][-1]]}\n",
"\n",
"\n",
"# Define the graph.\n",
"super_graph = StateGraph(State)\n",
"# First add the nodes, which will do the work\n",
"super_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\n",
"super_graph.add_node(\n",
" \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n",
")\n",
"super_graph.add_node(\"supervisor\", supervisor_node)\n",
"\n",
"# Define the graph connections, which controls how the logic\n",
"# propagates through the program\n",
"super_graph.add_edge(\"ResearchTeam\", \"supervisor\")\n",
"super_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\n",
"super_graph.add_conditional_edges(\n",
" \"supervisor\",\n",
" lambda x: x[\"next\"],\n",
" {\n",
" \"PaperWritingTeam\": \"PaperWritingTeam\",\n",
" \"ResearchTeam\": \"ResearchTeam\",\n",
" \"FINISH\": END,\n",
" },\n",
")\n",
"super_graph.set_entry_point(\"supervisor\")\n",
"super_graph = super_graph.compile()"
]
"source": ["# Top-level graph state\nclass State(TypedDict):\n messages: Annotated[List[BaseMessage], operator.add]\n next: str\n\n\ndef get_last_message(state: State) -> str:\n return state[\"messages\"][-1].content\n\n\ndef join_graph(response: dict):\n return {\"messages\": [response[\"messages\"][-1]]}\n\n\n# Define the graph.\nsuper_graph = StateGraph(State)\n# First add the nodes, which will do the work\nsuper_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\nsuper_graph.add_node(\n \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n)\nsuper_graph.add_node(\"supervisor\", supervisor_node)\n\n# Define the graph connections, which controls how the logic\n# propagates through the program\nsuper_graph.add_edge(\"ResearchTeam\", \"supervisor\")\nsuper_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\nsuper_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"PaperWritingTeam\": \"PaperWritingTeam\",\n \"ResearchTeam\": \"ResearchTeam\",\n \"FINISH\": END,\n },\n)\nsuper_graph.add_edge(START, \"supervisor\")\nsuper_graph = super_graph.compile()"]
},
{
"cell_type": "code",
@@ -869,11 +374,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(super_graph.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(super_graph.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -886,21 +387,7 @@
}
},
"outputs": [],
"source": [
"for s in super_graph.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n",
" )\n",
" ],\n",
" },\n",
" {\"recursion_limit\": 150},\n",
"):\n",
" if \"__end__\" not in s:\n",
" print(s)\n",
" print(\"---\")"
]
"source": ["for s in super_graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n )\n ],\n },\n {\"recursion_limit\": 150},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"]
}
],
"metadata": {
@@ -26,10 +26,7 @@
"id": "0d7b6dcc-c985-46e2-8457-7e6b0298b950",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core"
]
"source": ["%%capture --no-stderr\n%pip install -U langchain langchain_openai langsmith pandas langchain_experimental matplotlib langgraph langchain_core"]
},
{
"cell_type": "code",
@@ -37,24 +34,7 @@
"id": "743c19df-6da9-4d1e-b2d2-ea40080b9fdc",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
"\n",
"\n",
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")\n",
"\n",
"# Optional, add tracing in LangSmith\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""]
},
{
"cell_type": "markdown",
@@ -74,38 +54,7 @@
"id": "4325a10e-38dc-4a98-9004-e1525eaba377",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import (\n",
" BaseMessage,\n",
" HumanMessage,\n",
" ToolMessage,\n",
")\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"\n",
"def create_agent(llm, tools, system_message: str):\n",
" \"\"\"Create an agent.\"\"\"\n",
" prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a helpful AI assistant, collaborating with other assistants.\"\n",
" \" Use the provided tools to progress towards answering the question.\"\n",
" \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n",
" \" will help where you left off. Execute what you can to make progress.\"\n",
" \" If you or any of the other assistants have the final answer or deliverable,\"\n",
" \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n",
" \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
" )\n",
" prompt = prompt.partial(system_message=system_message)\n",
" prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n",
" return prompt | llm.bind_tools(tools)"
]
"source": ["from langchain_core.messages import (\n BaseMessage,\n HumanMessage,\n ToolMessage,\n)\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(llm, tools, system_message: str):\n \"\"\"Create an agent.\"\"\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful AI assistant, collaborating with other assistants.\"\n \" Use the provided tools to progress towards answering the question.\"\n \" If you are unable to fully answer, that's OK, another assistant with different tools \"\n \" will help where you left off. Execute what you can to make progress.\"\n \" If you or any of the other assistants have the final answer or deliverable,\"\n \" prefix your response with FINAL ANSWER so the team knows to stop.\"\n \" You have access to the following tools: {tool_names}.\\n{system_message}\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n )\n prompt = prompt.partial(system_message=system_message)\n prompt = prompt.partial(tool_names=\", \".join([tool.name for tool in tools]))\n return prompt | llm.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -123,35 +72,7 @@
"id": "ca076f3b-a729-4ca9-8f91-05c2ba58d610",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.tools import tool\n",
"from langchain_experimental.utilities import PythonREPL\n",
"\n",
"tavily_tool = TavilySearchResults(max_results=5)\n",
"\n",
"# Warning: This executes code locally, which can be unsafe when not sandboxed\n",
"\n",
"repl = PythonREPL()\n",
"\n",
"\n",
"@tool\n",
"def python_repl(\n",
" code: Annotated[str, \"The python code to execute to generate your chart.\"],\n",
"):\n",
" \"\"\"Use this to execute python code. If you want to see the output of a value,\n",
" you should print it out with `print(...)`. This is visible to the user.\"\"\"\n",
" try:\n",
" result = repl.run(code)\n",
" except BaseException as e:\n",
" return f\"Failed to execute. Error: {repr(e)}\"\n",
" result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n",
" return (\n",
" result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n",
" )"
]
"source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\nfrom langchain_experimental.utilities import PythonREPL\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n result_str = f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"\n return (\n result_str + \"\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\"\n )"]
},
{
"cell_type": "markdown",
@@ -179,19 +100,7 @@
"id": "290c91d4-f6f4-443c-8181-233d39102974",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"\n",
"# This defines the object that is passed between each node\n",
"# in the graph. We will create different nodes for each agent and tool\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]\n",
" sender: str"
]
"source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_openai import ChatOpenAI\n\n\n# This defines the object that is passed between each node\n# in the graph. We will create different nodes for each agent and tool\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n sender: str"]
},
{
"cell_type": "markdown",
@@ -209,46 +118,7 @@
"id": "71b790ca-9cef-4b22-b469-4b1d5d8424d6",
"metadata": {},
"outputs": [],
"source": [
"import functools\n",
"\n",
"from langchain_core.messages import AIMessage\n",
"\n",
"\n",
"# Helper function to create a node for a given agent\n",
"def agent_node(state, agent, name):\n",
" result = agent.invoke(state)\n",
" # We convert the agent output into a format that is suitable to append to the global state\n",
" if isinstance(result, ToolMessage):\n",
" pass\n",
" else:\n",
" result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n",
" return {\n",
" \"messages\": [result],\n",
" # Since we have a strict workflow, we can\n",
" # track the sender so we know who to pass to next.\n",
" \"sender\": name,\n",
" }\n",
"\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n",
"\n",
"# Research agent and node\n",
"research_agent = create_agent(\n",
" llm,\n",
" [tavily_tool],\n",
" system_message=\"You should provide accurate data for the chart_generator to use.\",\n",
")\n",
"research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n",
"\n",
"# chart_generator\n",
"chart_agent = create_agent(\n",
" llm,\n",
" [python_repl],\n",
" system_message=\"Any charts you display will be visible by the user.\",\n",
")\n",
"chart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")"
]
"source": ["import functools\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to create a node for a given agent\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n # We convert the agent output into a format that is suitable to append to the global state\n if isinstance(result, ToolMessage):\n pass\n else:\n result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n return {\n \"messages\": [result],\n # Since we have a strict workflow, we can\n # track the sender so we know who to pass to next.\n \"sender\": name,\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\n# Research agent and node\nresearch_agent = create_agent(\n llm,\n [tavily_tool],\n system_message=\"You should provide accurate data for the chart_generator to use.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# chart_generator\nchart_agent = create_agent(\n llm,\n [python_repl],\n system_message=\"Any charts you display will be visible by the user.\",\n)\nchart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")"]
},
{
"cell_type": "markdown",
@@ -266,12 +136,7 @@
"id": "d9a79c76-5c7c-42f6-91cf-635bc8305804",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tools = [tavily_tool, python_repl]\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntools = [tavily_tool, python_repl]\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -289,23 +154,7 @@
"id": "4f4b4d37-e8a3-4abb-8d42-eaea26016f35",
"metadata": {},
"outputs": [],
"source": [
"# Either agent can decide to end\n",
"from typing import Literal\n",
"\n",
"\n",
"def router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n",
" # This is the router\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" if last_message.tool_calls:\n",
" # The previous agent is invoking a tool\n",
" return \"call_tool\"\n",
" if \"FINAL ANSWER\" in last_message.content:\n",
" # Any agent decided the work is done\n",
" return \"__end__\"\n",
" return \"continue\""
]
"source": ["# Either agent can decide to end\nfrom typing import Literal\n\n\ndef router(state) -> Literal[\"call_tool\", \"__end__\", \"continue\"]:\n # This is the router\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message.tool_calls:\n # The previous agent is invoking a tool\n return \"call_tool\"\n if \"FINAL ANSWER\" in last_message.content:\n # Any agent decided the work is done\n return \"__end__\"\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -323,39 +172,7 @@
"id": "4dce3901-6ad5-4df5-8528-6e865cf96cb0",
"metadata": {},
"outputs": [],
"source": [
"workflow = StateGraph(AgentState)\n",
"\n",
"workflow.add_node(\"Researcher\", research_node)\n",
"workflow.add_node(\"chart_generator\", chart_node)\n",
"workflow.add_node(\"call_tool\", tool_node)\n",
"\n",
"workflow.add_conditional_edges(\n",
" \"Researcher\",\n",
" router,\n",
" {\"continue\": \"chart_generator\", \"call_tool\": \"call_tool\", \"__end__\": END},\n",
")\n",
"workflow.add_conditional_edges(\n",
" \"chart_generator\",\n",
" router,\n",
" {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"__end__\": END},\n",
")\n",
"\n",
"workflow.add_conditional_edges(\n",
" \"call_tool\",\n",
" # Each agent node updates the 'sender' field\n",
" # the tool calling node does not, meaning\n",
" # this edge will route back to the original agent\n",
" # who invoked the tool\n",
" lambda x: x[\"sender\"],\n",
" {\n",
" \"Researcher\": \"Researcher\",\n",
" \"chart_generator\": \"chart_generator\",\n",
" },\n",
")\n",
"workflow.set_entry_point(\"Researcher\")\n",
"graph = workflow.compile()"
]
"source": ["workflow = StateGraph(AgentState)\n\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"chart_generator\", chart_node)\nworkflow.add_node(\"call_tool\", tool_node)\n\nworkflow.add_conditional_edges(\n \"Researcher\",\n router,\n {\"continue\": \"chart_generator\", \"call_tool\": \"call_tool\", \"__end__\": END},\n)\nworkflow.add_conditional_edges(\n \"chart_generator\",\n router,\n {\"continue\": \"Researcher\", \"call_tool\": \"call_tool\", \"__end__\": END},\n)\n\nworkflow.add_conditional_edges(\n \"call_tool\",\n # Each agent node updates the 'sender' field\n # the tool calling node does not, meaning\n # this edge will route back to the original agent\n # who invoked the tool\n lambda x: x[\"sender\"],\n {\n \"Researcher\": \"Researcher\",\n \"chart_generator\": \"chart_generator\",\n },\n)\nworkflow.add_edge(START, \"Researcher\")\ngraph = workflow.compile()"]
},
{
"cell_type": "code",
@@ -374,15 +191,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "markdown",
@@ -481,24 +290,7 @@
]
}
],
"source": [
"events = graph.stream(\n",
" {\n",
" \"messages\": [\n",
" HumanMessage(\n",
" content=\"Fetch the UK's GDP over the past 5 years,\"\n",
" \" then draw a line graph of it.\"\n",
" \" Once you code it up, finish.\"\n",
" )\n",
" ],\n",
" },\n",
" # Maximum number of steps to take in the graph\n",
" {\"recursion_limit\": 150},\n",
")\n",
"for s in events:\n",
" print(s)\n",
" print(\"----\")"
]
"source": ["events = graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Fetch the UK's GDP over the past 5 years,\"\n \" then draw a line graph of it.\"\n \" Once you code it up, finish.\"\n )\n ],\n },\n # Maximum number of steps to take in the graph\n {\"recursion_limit\": 150},\n)\nfor s in events:\n print(s)\n print(\"----\")"]
},
{
"cell_type": "code",
@@ -506,7 +298,7 @@
"id": "010fc36e-4116-4758-bcac-b02c7dcd405d",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+12 -234
View File
@@ -35,10 +35,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai"]
},
{
"cell_type": "markdown",
@@ -54,13 +51,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"if \"OPENAI_API_KEY\" not in os.environ:\n",
" os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"
]
"source": ["import getpass\nimport os\n\nif \"OPENAI_API_KEY\" not in os.environ:\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"]
},
{
"cell_type": "markdown",
@@ -76,12 +67,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"\n",
"if \"LANGCHAIN_API_KEY\" not in os.environ:\n",
" os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n\nif \"LANGCHAIN_API_KEY\" not in os.environ:\n os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"]
},
{
"cell_type": "markdown",
@@ -102,36 +88,7 @@
"id": "1d36e782-80f4-4334-b7d7-ee4c79864480",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"from langchain_core.tools import BaseTool, tool\n",
"\n",
"# A global dict that the tools will be updating in this example.\n",
"user_to_pets = {}\n",
"\n",
"\n",
"def generate_tools_for_user(user_id: str) -> List[BaseTool]:\n",
" \"\"\"Generate a set of tools that have a user id associated with them.\"\"\"\n",
"\n",
" @tool\n",
" def update_favorite_pets(pets: List[str]) -> None:\n",
" \"\"\"Add the list of favorite pets.\"\"\"\n",
" user_to_pets[user_id] = pets\n",
"\n",
" @tool\n",
" def delete_favorite_pets() -> None:\n",
" \"\"\"Delete the list of favorite pets.\"\"\"\n",
" if user_id in user_to_pets:\n",
" del user_to_pets[user_id]\n",
"\n",
" @tool\n",
" def list_favorite_pets() -> None:\n",
" \"\"\"List favorite pets if any.\"\"\"\n",
" return user_to_pets.get(user_id, [])\n",
"\n",
" return [update_favorite_pets, delete_favorite_pets, list_favorite_pets]"
]
"source": ["from typing import List\n\nfrom langchain_core.tools import BaseTool, tool\n\n# A global dict that the tools will be updating in this example.\nuser_to_pets = {}\n\n\ndef generate_tools_for_user(user_id: str) -> List[BaseTool]:\n \"\"\"Generate a set of tools that have a user id associated with them.\"\"\"\n\n @tool\n def update_favorite_pets(pets: List[str]) -> None:\n \"\"\"Add the list of favorite pets.\"\"\"\n user_to_pets[user_id] = pets\n\n @tool\n def delete_favorite_pets() -> None:\n \"\"\"Delete the list of favorite pets.\"\"\"\n if user_id in user_to_pets:\n del user_to_pets[user_id]\n\n @tool\n def list_favorite_pets() -> None:\n \"\"\"List favorite pets if any.\"\"\"\n return user_to_pets.get(user_id, [])\n\n return [update_favorite_pets, delete_favorite_pets, list_favorite_pets]"]
},
{
"cell_type": "markdown",
@@ -155,13 +112,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"# We will set streaming=True so that we can stream tokens\n",
"# See the streaming section for more information on this.\n",
"model = ChatOpenAI(temperature=0, streaming=True)"
]
"source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"]
},
{
"cell_type": "markdown",
@@ -187,16 +138,7 @@
"id": "ea793afa-2eab-4901-910d-6eed90cd6564",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"]
},
{
"cell_type": "markdown",
@@ -231,69 +173,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolExecutor, ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state, config):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state, config):\n",
" messages = state[\"messages\"]\n",
" tools = generate_tools_for_user(config[\"user_id\"])\n",
" model_with_tools = model.bind_tools(tools)\n",
" response = model_with_tools.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state, config):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation for each tool call\n",
" tool_invocations = []\n",
" for tool_call in last_message.tool_calls:\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" tool_invocations.append(action)\n",
"\n",
" # We call the tool_executor and get back a response\n",
" # We can now wrap these tools in a simple ToolExecutor.\n",
" # This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n",
" # A ToolInvocation is any class with `tool` and `tool_input` attribute.\n",
" tools = generate_tools_for_user(config[\"user_id\"])\n",
" tool_executor = ToolExecutor(tools)\n",
" responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n",
" # We use the response to create tool messages\n",
" tool_messages = [\n",
" ToolMessage(\n",
" content=str(response),\n",
" name=tc[\"name\"],\n",
" tool_call_id=tc[\"id\"],\n",
" )\n",
" for tc, response in zip(last_message.tool_calls, responses)\n",
" ]\n",
"\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": tool_messages}"
]
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolExecutor, ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state, config):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state, config):\n messages = state[\"messages\"]\n tools = generate_tools_for_user(config[\"user_id\"])\n model_with_tools = model.bind_tools(tools)\n response = model_with_tools.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state, config):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n # We call the tool_executor and get back a response\n # We can now wrap these tools in a simple ToolExecutor.\n # This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n # A ToolInvocation is any class with `tool` and `tool_input` attribute.\n tools = generate_tools_for_user(config[\"user_id\"])\n tool_executor = ToolExecutor(tools)\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"]
},
{
"cell_type": "markdown",
@@ -311,50 +191,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -373,15 +210,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "markdown",
@@ -427,24 +256,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"user_to_pets.clear() # Clear the state\n",
"\n",
"print(f\"User information prior to run: {user_to_pets}\")\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"my favorite pets are cats and dogs\")]}\n",
"for output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value)\n",
" print(\"\\n---\\n\")\n",
"\n",
"print(f\"User information prior to run: {user_to_pets}\")"
]
"source": ["from langchain_core.messages import HumanMessage\n\nuser_to_pets.clear() # Clear the state\n\nprint(f\"User information prior to run: {user_to_pets}\")\n\ninputs = {\"messages\": [HumanMessage(content=\"my favorite pets are cats and dogs\")]}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\nprint(f\"User information prior to run: {user_to_pets}\")"]
},
{
"cell_type": "code",
@@ -479,22 +291,7 @@
]
}
],
"source": [
"print(f\"User information prior to run: {user_to_pets}\")\n",
"\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"what are my favorite pets?\")]}\n",
"for output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value)\n",
" print(\"\\n---\\n\")\n",
"\n",
"\n",
"print(f\"User information prior to run: {user_to_pets}\")"
]
"source": ["print(f\"User information prior to run: {user_to_pets}\")\n\n\ninputs = {\"messages\": [HumanMessage(content=\"what are my favorite pets?\")]}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\n\nprint(f\"User information prior to run: {user_to_pets}\")"]
},
{
"cell_type": "code",
@@ -529,26 +326,7 @@
]
}
],
"source": [
"print(f\"User information prior to run: {user_to_pets}\")\n",
"\n",
"\n",
"inputs = {\n",
" \"messages\": [\n",
" HumanMessage(content=\"please forget what i told you about my favorite animals\")\n",
" ]\n",
"}\n",
"for output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n",
" # stream() yields dictionaries with output keyed by node name\n",
" for key, value in output.items():\n",
" print(f\"Output from node '{key}':\")\n",
" print(\"---\")\n",
" print(value)\n",
" print(\"\\n---\\n\")\n",
"\n",
"\n",
"print(f\"User information prior to run: {user_to_pets}\")"
]
"source": ["print(f\"User information prior to run: {user_to_pets}\")\n\n\ninputs = {\n \"messages\": [\n HumanMessage(content=\"please forget what i told you about my favorite animals\")\n ]\n}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\n\nprint(f\"User information prior to run: {user_to_pets}\")"]
}
],
"metadata": {
+19 -165
View File
@@ -44,7 +44,7 @@
"id": "0c3fde0a",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
},
{
"cell_type": "markdown",
@@ -62,10 +62,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
},
{
"cell_type": "markdown",
@@ -81,18 +78,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -108,10 +94,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -129,22 +112,7 @@
"id": "14619607",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# Add messages essentially does this with more\n",
"# robust handling\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
},
{
"cell_type": "markdown",
@@ -164,19 +132,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" return [\"The answer to your question lies within.\"]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -193,11 +149,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -227,13 +179,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"# We will set streaming=True so that we can stream tokens\n",
"# See the streaming section for more information on this.\n",
"model = ChatOpenAI(temperature=0, streaming=True)"
]
"source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"]
},
{
"cell_type": "markdown",
@@ -251,9 +197,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"bound_model = model.bind_tools(tools)"
]
"source": ["bound_model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -288,27 +232,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"# Define the function that determines whether to continue or not\n",
"from typing import Literal\n",
"\n",
"\n",
"def should_continue(state: State) -> Literal[\"action\", \"__end__\"]:\n",
" \"\"\"Return the next node to execute.\"\"\"\n",
" last_message = state[\"messages\"][-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"__end__\"\n",
" # Otherwise if there is, we continue\n",
" return \"action\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state: State):\n",
" response = model.invoke(state[\"messages\"])\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": response}"
]
"source": ["# Define the function that determines whether to continue or not\nfrom typing import Literal\n\n\ndef should_continue(state: State) -> Literal[\"action\", \"__end__\"]:\n \"\"\"Return the next node to execute.\"\"\"\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"__end__\"\n # Otherwise if there is, we continue\n return \"action\"\n\n\n# Define the function that calls the model\ndef call_model(state: State):\n response = model.invoke(state[\"messages\"])\n # We return a list, because this will get added to the existing list\n return {\"messages\": response}"]
},
{
"cell_type": "markdown",
@@ -324,33 +248,7 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")"
]
"source": ["from langgraph.graph import StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
},
{
"cell_type": "markdown",
@@ -368,11 +266,7 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
]
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
},
{
"cell_type": "code",
@@ -380,12 +274,7 @@
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
"metadata": {},
"outputs": [],
"source": [
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile(checkpointer=memory)"
]
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
@@ -404,15 +293,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(app.get_graph().draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "markdown",
@@ -443,14 +324,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"hi! I'm bob\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -471,11 +345,7 @@
]
}
],
"source": [
"input_message = HumanMessage(content=\"what is my name?\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["input_message = HumanMessage(content=\"what is my name?\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -504,15 +374,7 @@
]
}
],
"source": [
"input_message = HumanMessage(content=\"what is my name?\")\n",
"for event in app.stream(\n",
" {\"messages\": [input_message]},\n",
" {\"configurable\": {\"thread_id\": \"3\"}},\n",
" stream_mode=\"values\",\n",
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["input_message = HumanMessage(content=\"what is my name?\")\nfor event in app.stream(\n {\"messages\": [input_message]},\n {\"configurable\": {\"thread_id\": \"3\"}},\n stream_mode=\"values\",\n):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -541,15 +403,7 @@
]
}
],
"source": [
"input_message = HumanMessage(content=\"You forgot??\")\n",
"for event in app.stream(\n",
" {\"messages\": [input_message]},\n",
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
" stream_mode=\"values\",\n",
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["input_message = HumanMessage(content=\"You forgot??\")\nfor event in app.stream(\n {\"messages\": [input_message]},\n {\"configurable\": {\"thread_id\": \"2\"}},\n stream_mode=\"values\",\n):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -557,7 +411,7 @@
"id": "eb20430f",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+16 -218
View File
@@ -45,10 +45,7 @@
"id": "b451b58a-89bd-424f-8c06-0d9fe325e01b",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python"]
},
{
"cell_type": "markdown",
@@ -64,19 +61,7 @@
"id": "ce438281-08d5-4804-afe7-e4089f7b016b",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")\n",
"_set_env(\"TAVILY_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -92,11 +77,7 @@
"id": "01f460d1-f26f-47d1-ae76-de74d5d851de",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Plan-and-execute\""
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Plan-and-execute\""]
},
{
"cell_type": "markdown",
@@ -114,11 +95,7 @@
"id": "25b9ec62-0675-4715-811c-9b32c635b22f",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"tools = [TavilySearchResults(max_results=3)]"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntools = [TavilySearchResults(max_results=3)]"]
},
{
"cell_type": "markdown",
@@ -151,20 +128,7 @@
]
}
],
"source": [
"from langchain import hub\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"# Get the prompt to use - you can modify this!\n",
"prompt = hub.pull(\"wfh/react-agent-executor\")\n",
"prompt.pretty_print()\n",
"\n",
"# Choose the LLM that will drive the agent\n",
"llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
"agent_executor = create_react_agent(llm, tools, messages_modifier=prompt)"
]
"source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import create_react_agent\n\n# Get the prompt to use - you can modify this!\nprompt = hub.pull(\"wfh/react-agent-executor\")\nprompt.pretty_print()\n\n# Choose the LLM that will drive the agent\nllm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\nagent_executor = create_react_agent(llm, tools, messages_modifier=prompt)"]
},
{
"cell_type": "code",
@@ -186,9 +150,7 @@
"output_type": "execute_result"
}
],
"source": [
"agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})"
]
"source": ["agent_executor.invoke({\"messages\": [(\"user\", \"who is the winnner of the us open\")]})"]
},
{
"cell_type": "markdown",
@@ -212,17 +174,7 @@
"id": "8eeeaeea-8f10-4fbe-8e24-4e1a2381a009",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, List, Tuple, TypedDict\n",
"\n",
"\n",
"class PlanExecute(TypedDict):\n",
" input: str\n",
" plan: List[str]\n",
" past_steps: Annotated[List[Tuple], operator.add]\n",
" response: str"
]
"source": ["import operator\nfrom typing import Annotated, List, Tuple, TypedDict\n\n\nclass PlanExecute(TypedDict):\n input: str\n plan: List[str]\n past_steps: Annotated[List[Tuple], operator.add]\n response: str"]
},
{
"cell_type": "markdown",
@@ -240,17 +192,7 @@
"id": "4a88626d-6dfd-4488-87f0-a9a0dd6da44c",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"\n",
"class Plan(BaseModel):\n",
" \"\"\"Plan to follow in future\"\"\"\n",
"\n",
" steps: List[str] = Field(\n",
" description=\"different steps to follow, should be in sorted order\"\n",
" )"
]
"source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Plan(BaseModel):\n \"\"\"Plan to follow in future\"\"\"\n\n steps: List[str] = Field(\n description=\"different steps to follow, should be in sorted order\"\n )"]
},
{
"cell_type": "code",
@@ -258,24 +200,7 @@
"id": "ec7b1867-1ea3-4df3-9a98-992a1c32ec49",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.prompts import ChatPromptTemplate\n",
"\n",
"planner_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"For the given objective, come up with a simple step by step plan. \\\n",
"This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n",
"The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\"\"\",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"planner = planner_prompt | ChatOpenAI(\n",
" model=\"gpt-4o\", temperature=0\n",
").with_structured_output(Plan)"
]
"source": ["from langchain_core.prompts import ChatPromptTemplate\n\nplanner_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"For the given objective, come up with a simple step by step plan. \\\nThis plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\nThe result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\"\"\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n)\nplanner = planner_prompt | ChatOpenAI(\n model=\"gpt-4o\", temperature=0\n).with_structured_output(Plan)"]
},
{
"cell_type": "code",
@@ -294,15 +219,7 @@
"output_type": "execute_result"
}
],
"source": [
"planner.invoke(\n",
" {\n",
" \"messages\": [\n",
" (\"user\", \"what is the hometown of the current Australia open winner?\")\n",
" ]\n",
" }\n",
")"
]
"source": ["planner.invoke(\n {\n \"messages\": [\n (\"user\", \"what is the hometown of the current Australia open winner?\")\n ]\n }\n)"]
},
{
"cell_type": "markdown",
@@ -320,47 +237,7 @@
"id": "ec2d12cc-016a-44d1-aa08-4c5ce1e8fe2a",
"metadata": {},
"outputs": [],
"source": [
"from typing import Union\n",
"\n",
"\n",
"class Response(BaseModel):\n",
" \"\"\"Response to user.\"\"\"\n",
"\n",
" response: str\n",
"\n",
"\n",
"class Act(BaseModel):\n",
" \"\"\"Action to perform.\"\"\"\n",
"\n",
" action: Union[Response, Plan] = Field(\n",
" description=\"Action to perform. If you want to respond to user, use Response. \"\n",
" \"If you need to further use tools to get the answer, use Plan.\"\n",
" )\n",
"\n",
"\n",
"replanner_prompt = ChatPromptTemplate.from_template(\n",
" \"\"\"For the given objective, come up with a simple step by step plan. \\\n",
"This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\n",
"The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n",
"\n",
"Your objective was this:\n",
"{input}\n",
"\n",
"Your original plan was this:\n",
"{plan}\n",
"\n",
"You have currently done the follow steps:\n",
"{past_steps}\n",
"\n",
"Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.\"\"\"\n",
")\n",
"\n",
"\n",
"replanner = replanner_prompt | ChatOpenAI(\n",
" model=\"gpt-4o\", temperature=0\n",
").with_structured_output(Act)"
]
"source": ["from typing import Union\n\n\nclass Response(BaseModel):\n \"\"\"Response to user.\"\"\"\n\n response: str\n\n\nclass Act(BaseModel):\n \"\"\"Action to perform.\"\"\"\n\n action: Union[Response, Plan] = Field(\n description=\"Action to perform. If you want to respond to user, use Response. \"\n \"If you need to further use tools to get the answer, use Plan.\"\n )\n\n\nreplanner_prompt = ChatPromptTemplate.from_template(\n \"\"\"For the given objective, come up with a simple step by step plan. \\\nThis plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \\\nThe result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.\n\nYour objective was this:\n{input}\n\nYour original plan was this:\n{plan}\n\nYou have currently done the follow steps:\n{past_steps}\n\nUpdate your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.\"\"\"\n)\n\n\nreplanner = replanner_prompt | ChatOpenAI(\n model=\"gpt-4o\", temperature=0\n).with_structured_output(Act)"]
},
{
"cell_type": "markdown",
@@ -378,43 +255,7 @@
"id": "6c8e0dad-bcea-4c9a-8922-0d820892e2d0",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"\n",
"async def execute_step(state: PlanExecute):\n",
" plan = state[\"plan\"]\n",
" plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n",
" task = plan[0]\n",
" task_formatted = f\"\"\"For the following plan:\n",
"{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n",
" agent_response = await agent_executor.ainvoke(\n",
" {\"messages\": [(\"user\", task_formatted)]}\n",
" )\n",
" return {\n",
" \"past_steps\": (task, agent_response[\"messages\"][-1].content),\n",
" }\n",
"\n",
"\n",
"async def plan_step(state: PlanExecute):\n",
" plan = await planner.ainvoke({\"messages\": [(\"user\", state[\"input\"])]})\n",
" return {\"plan\": plan.steps}\n",
"\n",
"\n",
"async def replan_step(state: PlanExecute):\n",
" output = await replanner.ainvoke(state)\n",
" if isinstance(output.action, Response):\n",
" return {\"response\": output.action.response}\n",
" else:\n",
" return {\"plan\": output.action.steps}\n",
"\n",
"\n",
"def should_end(state: PlanExecute) -> Literal[\"agent\", \"__end__\"]:\n",
" if \"response\" in state and state[\"response\"]:\n",
" return \"__end__\"\n",
" else:\n",
" return \"agent\""
]
"source": ["from typing import Literal\n\n\nasync def execute_step(state: PlanExecute):\n plan = state[\"plan\"]\n plan_str = \"\\n\".join(f\"{i+1}. {step}\" for i, step in enumerate(plan))\n task = plan[0]\n task_formatted = f\"\"\"For the following plan:\n{plan_str}\\n\\nYou are tasked with executing step {1}, {task}.\"\"\"\n agent_response = await agent_executor.ainvoke(\n {\"messages\": [(\"user\", task_formatted)]}\n )\n return {\n \"past_steps\": (task, agent_response[\"messages\"][-1].content),\n }\n\n\nasync def plan_step(state: PlanExecute):\n plan = await planner.ainvoke({\"messages\": [(\"user\", state[\"input\"])]})\n return {\"plan\": plan.steps}\n\n\nasync def replan_step(state: PlanExecute):\n output = await replanner.ainvoke(state)\n if isinstance(output.action, Response):\n return {\"response\": output.action.response}\n else:\n return {\"plan\": output.action.steps}\n\n\ndef should_end(state: PlanExecute) -> Literal[\"agent\", \"__end__\"]:\n if \"response\" in state and state[\"response\"]:\n return \"__end__\"\n else:\n return \"agent\""]
},
{
"cell_type": "code",
@@ -422,39 +263,7 @@
"id": "e954cea0-5ccc-46c2-a27b-f5b7185b597d",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph\n",
"\n",
"workflow = StateGraph(PlanExecute)\n",
"\n",
"# Add the plan node\n",
"workflow.add_node(\"planner\", plan_step)\n",
"\n",
"# Add the execution step\n",
"workflow.add_node(\"agent\", execute_step)\n",
"\n",
"# Add a replan node\n",
"workflow.add_node(\"replan\", replan_step)\n",
"\n",
"workflow.set_entry_point(\"planner\")\n",
"\n",
"# From plan we go to agent\n",
"workflow.add_edge(\"planner\", \"agent\")\n",
"\n",
"# From agent, we replan\n",
"workflow.add_edge(\"agent\", \"replan\")\n",
"\n",
"workflow.add_conditional_edges(\n",
" \"replan\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_end,\n",
")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import StateGraph, START\n\nworkflow = StateGraph(PlanExecute)\n\n# Add the plan node\nworkflow.add_node(\"planner\", plan_step)\n\n# Add the execution step\nworkflow.add_node(\"agent\", execute_step)\n\n# Add a replan node\nworkflow.add_node(\"replan\", replan_step)\n\nworkflow.add_edge(START, \"planner\")\n\n# From plan we go to agent\nworkflow.add_edge(\"planner\", \"agent\")\n\n# From agent, we replan\nworkflow.add_edge(\"agent\", \"replan\")\n\nworkflow.add_conditional_edges(\n \"replan\",\n # Next, we pass in the function that will determine which node is called next.\n should_end,\n)\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -473,11 +282,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -505,14 +310,7 @@
]
}
],
"source": [
"config = {\"recursion_limit\": 50}\n",
"inputs = {\"input\": \"what is the hometown of the 2024 Australia open winner?\"}\n",
"async for event in app.astream(inputs, config=config):\n",
" for k, v in event.items():\n",
" if k != \"__end__\":\n",
" print(v)"
]
"source": ["config = {\"recursion_limit\": 50}\ninputs = {\"input\": \"what is the hometown of the 2024 Australia open winner?\"}\nasync for event in app.astream(inputs, config=config):\n for k, v in event.items():\n if k != \"__end__\":\n print(v)"]
},
{
"cell_type": "markdown",
@@ -530,7 +328,7 @@
"id": "ad8f7955-2cc9-4ebb-8c41-13abb3351a24",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -307
View File
@@ -20,10 +20,7 @@
"id": "969fb438",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"
]
"source": ["%%capture --no-stderr\n%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"]
},
{
"cell_type": "code",
@@ -31,22 +28,7 @@
"id": "e4958a8c",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(key: str):\n",
" if key not in os.environ:\n",
" os.environ[key] = getpass.getpass(f\"{key}:\")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")\n",
"\n",
"# (Optional) For tracing\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(key: str):\n if key not in os.environ:\n os.environ[key] = getpass.getpass(f\"{key}:\")\n\n\n_set_env(\"OPENAI_API_KEY\")\n\n# (Optional) For tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -64,34 +46,7 @@
"id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import Chroma\n",
"from langchain_openai import OpenAIEmbeddings\n",
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
"\n",
"urls = [\n",
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
"]\n",
"\n",
"docs = [WebBaseLoader(url).load() for url in urls]\n",
"docs_list = [item for sublist in docs for item in sublist]\n",
"\n",
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
" chunk_size=100, chunk_overlap=50\n",
")\n",
"doc_splits = text_splitter.split_documents(docs_list)\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=OpenAIEmbeddings(),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
"source": ["from langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=100, chunk_overlap=50\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"]
},
{
"cell_type": "markdown",
@@ -107,17 +62,7 @@
"id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048",
"metadata": {},
"outputs": [],
"source": [
"from langchain.tools.retriever import create_retriever_tool\n",
"\n",
"retriever_tool = create_retriever_tool(\n",
" retriever,\n",
" \"retrieve_blog_posts\",\n",
" \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n",
")\n",
"\n",
"tools = [retriever_tool]"
]
"source": ["from langchain.tools.retriever import create_retriever_tool\n\nretriever_tool = create_retriever_tool(\n retriever,\n \"retrieve_blog_posts\",\n \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n)\n\ntools = [retriever_tool]"]
},
{
"cell_type": "markdown",
@@ -141,19 +86,7 @@
"id": "0e378706-47d5-425a-8ba0-57b9acffbd0c",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" # The add_messages function defines how an update should be processed\n",
" # Default is to replace. add_messages says \"append\"\n",
" messages: Annotated[Sequence[BaseMessage], add_messages]"
]
"source": ["from typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\nfrom langgraph.graph.message import add_messages\n\n\nclass AgentState(TypedDict):\n # The add_messages function defines how an update should be processed\n # Default is to replace. add_messages says \"append\"\n messages: Annotated[Sequence[BaseMessage], add_messages]"]
},
{
"attachments": {
@@ -196,174 +129,7 @@
]
}
],
"source": [
"from typing import Annotated, Literal, Sequence, TypedDict\n",
"\n",
"from langchain import hub\n",
"from langchain_core.messages import BaseMessage, HumanMessage\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"from langchain_core.prompts import PromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" str: A decision for whether the documents are relevant or not\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK RELEVANCE---\")\n",
"\n",
" # Data model\n",
" class grade(BaseModel):\n",
" \"\"\"Binary score for relevance check.\"\"\"\n",
"\n",
" binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n",
"\n",
" # LLM\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
"\n",
" # LLM with tool and validation\n",
" llm_with_tool = model.with_structured_output(grade)\n",
"\n",
" # Prompt\n",
" prompt = PromptTemplate(\n",
" template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" Here is the retrieved document: \\n\\n {context} \\n\\n\n",
" Here is the user question: {question} \\n\n",
" If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n",
" input_variables=[\"context\", \"question\"],\n",
" )\n",
"\n",
" # Chain\n",
" chain = prompt | llm_with_tool\n",
"\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
"\n",
" question = messages[0].content\n",
" docs = last_message.content\n",
"\n",
" scored_result = chain.invoke({\"question\": question, \"context\": docs})\n",
"\n",
" score = scored_result.binary_score\n",
"\n",
" if score == \"yes\":\n",
" print(\"---DECISION: DOCS RELEVANT---\")\n",
" return \"generate\"\n",
"\n",
" else:\n",
" print(\"---DECISION: DOCS NOT RELEVANT---\")\n",
" print(score)\n",
" return \"rewrite\"\n",
"\n",
"\n",
"### Nodes\n",
"\n",
"\n",
"def agent(state):\n",
" \"\"\"\n",
" Invokes the agent model to generate a response based on the current state. Given\n",
" the question, it will decide to retrieve using the retriever tool, or simply end.\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" dict: The updated state with the agent response appended to messages\n",
" \"\"\"\n",
" print(\"---CALL AGENT---\")\n",
" messages = state[\"messages\"]\n",
" model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n",
" model = model.bind_tools(tools)\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def rewrite(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" dict: The updated state with re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" messages = state[\"messages\"]\n",
" question = messages[0].content\n",
"\n",
" msg = [\n",
" HumanMessage(\n",
" content=f\"\"\" \\n \n",
" Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n",
" Here is the initial question:\n",
" \\n ------- \\n\n",
" {question} \n",
" \\n ------- \\n\n",
" Formulate an improved question: \"\"\",\n",
" )\n",
" ]\n",
"\n",
" # Grader\n",
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
" response = model.invoke(msg)\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (messages): The current state\n",
"\n",
" Returns:\n",
" dict: The updated state with re-phrased question\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" messages = state[\"messages\"]\n",
" question = messages[0].content\n",
" last_message = messages[-1]\n",
"\n",
" question = messages[0].content\n",
" docs = last_message.content\n",
"\n",
" # Prompt\n",
" prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
" # LLM\n",
" llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n",
"\n",
" # Post-processing\n",
" def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
" # Chain\n",
" rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
" # Run\n",
" response = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n",
"prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"
]
"source": ["from typing import Annotated, Literal, Sequence, TypedDict\n\nfrom langchain import hub\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import tools_condition\n\n### Edges\n\n\ndef grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (messages): The current state\n\n Returns:\n str: A decision for whether the documents are relevant or not\n \"\"\"\n\n print(\"---CHECK RELEVANCE---\")\n\n # Data model\n class grade(BaseModel):\n \"\"\"Binary score for relevance check.\"\"\"\n\n binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n\n # LLM\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n\n # LLM with tool and validation\n llm_with_tool = model.with_structured_output(grade)\n\n # Prompt\n prompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {context} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n input_variables=[\"context\", \"question\"],\n )\n\n # Chain\n chain = prompt | llm_with_tool\n\n messages = state[\"messages\"]\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n scored_result = chain.invoke({\"question\": question, \"context\": docs})\n\n score = scored_result.binary_score\n\n if score == \"yes\":\n print(\"---DECISION: DOCS RELEVANT---\")\n return \"generate\"\n\n else:\n print(\"---DECISION: DOCS NOT RELEVANT---\")\n print(score)\n return \"rewrite\"\n\n\n### Nodes\n\n\ndef agent(state):\n \"\"\"\n Invokes the agent model to generate a response based on the current state. Given\n the question, it will decide to retrieve using the retriever tool, or simply end.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with the agent response appended to messages\n \"\"\"\n print(\"---CALL AGENT---\")\n messages = state[\"messages\"]\n model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n model = model.bind_tools(tools)\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef rewrite(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n messages = state[\"messages\"]\n question = messages[0].content\n\n msg = [\n HumanMessage(\n content=f\"\"\" \\n \n Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n Here is the initial question:\n \\n ------- \\n\n {question} \n \\n ------- \\n\n Formulate an improved question: \"\"\",\n )\n ]\n\n # Grader\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n response = model.invoke(msg)\n return {\"messages\": [response]}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n print(\"---GENERATE---\")\n messages = state[\"messages\"]\n question = messages[0].content\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n # Prompt\n prompt = hub.pull(\"rlm/rag-prompt\")\n\n # LLM\n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n\n # Post-processing\n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n # Chain\n rag_chain = prompt | llm | StrOutputParser()\n\n # Run\n response = rag_chain.invoke({\"context\": docs, \"question\": question})\n return {\"messages\": [response]}\n\n\nprint(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\nprompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"]
},
{
"cell_type": "markdown",
@@ -384,48 +150,7 @@
"id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the nodes we will cycle between\n",
"workflow.add_node(\"agent\", agent) # agent\n",
"retrieve = ToolNode([retriever_tool])\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieval\n",
"workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n",
"workflow.add_node(\n",
" \"generate\", generate\n",
") # Generating a response after we know the documents are relevant\n",
"# Call agent node to decide to retrieve or not\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# Decide whether to retrieve\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" # Assess agent decision\n",
" tools_condition,\n",
" {\n",
" # Translate the condition outputs to nodes in our graph\n",
" \"tools\": \"retrieve\",\n",
" END: END,\n",
" },\n",
")\n",
"\n",
"# Edges taken after the `action` node is called.\n",
"workflow.add_conditional_edges(\n",
" \"retrieve\",\n",
" # Assess agent decision\n",
" grade_documents,\n",
")\n",
"workflow.add_edge(\"generate\", END)\n",
"workflow.add_edge(\"rewrite\", \"agent\")\n",
"\n",
"# Compile\n",
"graph = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import ToolNode\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the nodes we will cycle between\nworkflow.add_node(\"agent\", agent) # agent\nretrieve = ToolNode([retriever_tool])\nworkflow.add_node(\"retrieve\", retrieve) # retrieval\nworkflow.add_node(\"rewrite\", rewrite) # Re-writing the question\nworkflow.add_node(\n \"generate\", generate\n) # Generating a response after we know the documents are relevant\n# Call agent node to decide to retrieve or not\nworkflow.add_edge(START, \"agent\")\n\n# Decide whether to retrieve\nworkflow.add_conditional_edges(\n \"agent\",\n # Assess agent decision\n tools_condition,\n {\n # Translate the condition outputs to nodes in our graph\n \"tools\": \"retrieve\",\n END: END,\n },\n)\n\n# Edges taken after the `action` node is called.\nworkflow.add_conditional_edges(\n \"retrieve\",\n # Assess agent decision\n grade_documents,\n)\nworkflow.add_edge(\"generate\", END)\nworkflow.add_edge(\"rewrite\", \"agent\")\n\n# Compile\ngraph = workflow.compile()"]
},
{
"cell_type": "code",
@@ -444,15 +169,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "code",
@@ -486,21 +203,7 @@
]
}
],
"source": [
"import pprint\n",
"\n",
"inputs = {\n",
" \"messages\": [\n",
" (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n",
" ]\n",
"}\n",
"for output in graph.stream(inputs):\n",
" for key, value in output.items():\n",
" pprint.pprint(f\"Output from node '{key}':\")\n",
" pprint.pprint(\"---\")\n",
" pprint.pprint(value, indent=2, width=80, depth=None)\n",
" pprint.pprint(\"\\n---\\n\")"
]
"source": ["import pprint\n\ninputs = {\n \"messages\": [\n (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n ]\n}\nfor output in graph.stream(inputs):\n for key, value in output.items():\n pprint.pprint(f\"Output from node '{key}':\")\n pprint.pprint(\"---\")\n pprint.pprint(value, indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")"]
},
{
"cell_type": "code",
@@ -508,7 +211,7 @@
"id": "189333cc-5d34-4869-9f9b-741210e1096f",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+15 -370
View File
@@ -47,9 +47,7 @@
"id": "568c84d6-9df6-4b7b-b50d-476c0a64a04b",
"metadata": {},
"outputs": [],
"source": [
"! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python"
]
"source": ["! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph tavily-python"]
},
{
"cell_type": "markdown",
@@ -65,11 +63,7 @@
"id": "74710419-158d-4270-931c-de83db7b580d",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = \"<your-api-key>\""
]
"source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "markdown",
@@ -87,9 +81,7 @@
"id": "c3ac6e65-2d4e-48dd-9fff-40047373332d",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"TAVILY_API_KEY\"] = \"<your-api-key>\""
]
"source": ["os.environ[\"TAVILY_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "markdown",
@@ -107,11 +99,7 @@
"id": "e205f57e-5218-478b-ad8e-1723bdb0d45e",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "markdown",
@@ -129,34 +117,7 @@
"id": "3a566a30-cf0e-4330-ad4d-9bf994bdfa86",
"metadata": {},
"outputs": [],
"source": [
"from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import Chroma\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"urls = [\n",
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
"]\n",
"\n",
"docs = [WebBaseLoader(url).load() for url in urls]\n",
"docs_list = [item for sublist in docs for item in sublist]\n",
"\n",
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
" chunk_size=250, chunk_overlap=0\n",
")\n",
"doc_splits = text_splitter.split_documents(docs_list)\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=OpenAIEmbeddings(),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
"source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"]
},
{
"cell_type": "markdown",
@@ -180,44 +141,7 @@
]
}
],
"source": [
"### Retrieval Grader\n",
"\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"\n",
"# Data model\n",
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
"grade_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n",
" ]\n",
")\n",
"\n",
"retrieval_grader = grade_prompt | structured_llm_grader\n",
"question = \"agent memory\"\n",
"docs = retriever.get_relevant_documents(question)\n",
"doc_txt = docs[1].page_content\n",
"print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
]
"source": ["### Retrieval Grader\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
},
{
"cell_type": "code",
@@ -233,31 +157,7 @@
]
}
],
"source": [
"### Generate\n",
"\n",
"from langchain import hub\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"# Prompt\n",
"prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
"# Run\n",
"generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
"print(generation)"
]
"source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
},
{
"cell_type": "code",
@@ -276,28 +176,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Question Re-writer\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt\n",
"system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n",
"re_write_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\n",
" \"human\",\n",
" \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
"question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
"question_rewriter.invoke({\"question\": question})"
]
"source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for web search. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"]
},
{
"cell_type": "markdown",
@@ -313,13 +192,7 @@
"id": "46d51b53-54a9-4e0a-9f14-e39998f5b340",
"metadata": {},
"outputs": [],
"source": [
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
"source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"]
},
{
"cell_type": "markdown",
@@ -339,28 +212,7 @@
"id": "94b3945f-ef0f-458d-a443-f763903550b0",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" web_search: whether to add search\n",
" documents: list of documents\n",
" \"\"\"\n",
"\n",
" question: str\n",
" generation: str\n",
" web_search: str\n",
" documents: List[str]"
]
"source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n web_search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n web_search: str\n documents: List[str]"]
},
{
"cell_type": "code",
@@ -368,155 +220,7 @@
"id": "efd639c5-82e2-45e6-a94a-6a4039646ef5",
"metadata": {},
"outputs": [],
"source": [
"from langchain.schema import Document\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, documents, that contains retrieved documents\n",
" \"\"\"\n",
" print(\"---RETRIEVE---\")\n",
" question = state[\"question\"]\n",
"\n",
" # Retrieval\n",
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation, that contains LLM generation\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with only filtered relevant documents\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" web_search = \"No\"\n",
" for d in documents:\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
" else:\n",
" print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
" web_search = \"Yes\"\n",
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates question key with a re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with appended web results\n",
" \"\"\"\n",
"\n",
" print(\"---WEB SEARCH---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Web search\n",
" docs = web_search_tool.invoke({\"query\": question})\n",
" web_results = \"\\n\".join([d[\"content\"] for d in docs])\n",
" web_results = Document(page_content=web_results)\n",
" documents.append(web_results)\n",
"\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Binary decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---ASSESS GRADED DOCUMENTS---\")\n",
" state[\"question\"]\n",
" web_search = state[\"web_search\"]\n",
" state[\"documents\"]\n",
"\n",
" if web_search == \"Yes\":\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\""
]
"source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n web_search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n web_search = \"Yes\"\n continue\n return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n documents.append(web_results)\n\n return {\"documents\": documents, \"question\": question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n web_search = state[\"web_search\"]\n state[\"documents\"]\n\n if web_search == \"Yes\":\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\""]
},
{
"cell_type": "markdown",
@@ -534,36 +238,7 @@
"id": "dedae17a-98c6-474d-90a7-9234b7c8cea0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"workflow.add_node(\"web_search_node\", web_search) # web search\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
" decide_to_generate,\n",
" {\n",
" \"transform_query\": \"transform_query\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"transform_query\", \"web_search_node\")\n",
"workflow.add_edge(\"web_search_node\", \"generate\")\n",
"workflow.add_edge(\"generate\", END)\n",
"\n",
"# Compile\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\nworkflow.add_node(\"web_search_node\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"web_search_node\")\nworkflow.add_edge(\"web_search_node\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\n# Compile\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -606,22 +281,7 @@
]
}
],
"source": [
"from pprint import pprint\n",
"\n",
"# Run\n",
"inputs = {\"question\": \"What are the types of agent memory?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" # Optional: print full state at each node\n",
" # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "code",
@@ -666,22 +326,7 @@
]
}
],
"source": [
"from pprint import pprint\n",
"\n",
"# Run\n",
"inputs = {\"question\": \"How does the AlphaCodium paper work?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" # Optional: print full state at each node\n",
" # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"How does the AlphaCodium paper work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "markdown",
@@ -701,7 +346,7 @@
"id": "6ce65be5-fd12-4ffc-984c-34c132693e69",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+16 -458
View File
@@ -56,10 +56,7 @@
"id": "4a660963-bd3d-4c87-b2e4-b6e432055211",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai"]
},
{
"cell_type": "code",
@@ -67,11 +64,7 @@
"id": "68316ba0-854b-41e1-9af5-1f9e965946e3",
"metadata": {},
"outputs": [],
"source": [
"# Search\n",
"import os\n",
"os.environ[\"TAVILY_API_KEY\"] = \"xxx\""
]
"source": ["# Search\nimport os\nos.environ[\"TAVILY_API_KEY\"] = \"xxx\""]
},
{
"cell_type": "code",
@@ -79,10 +72,7 @@
"id": "0be68860-dded-481e-9fc7-a5042bf92c04",
"metadata": {},
"outputs": [],
"source": [
"# Embedding (optional)\n",
"os.environ[\"OPENAI_API_KEY\"] = \"xxx\""
]
"source": ["# Embedding (optional)\nos.environ[\"OPENAI_API_KEY\"] = \"xxx\""]
},
{
"cell_type": "code",
@@ -90,13 +80,7 @@
"id": "7248ab88-2b97-41eb-8dbb-4ea65525ed9a",
"metadata": {},
"outputs": [],
"source": [
"# Tracing and testing (optional)\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\""
]
"source": ["# Tracing and testing (optional)\nos.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\""]
},
{
"cell_type": "markdown",
@@ -114,11 +98,7 @@
"id": "2f4db331-c4d0-4c7c-a9a5-0bebc8a89c6c",
"metadata": {},
"outputs": [],
"source": [
"local_llm = \"llama3\"\n",
"model_tested = \"llama3-8b\"\n",
"metadata = f\"CRAG, {model_tested}\""
]
"source": ["local_llm = \"llama3\"\nmodel_tested = \"llama3-8b\"\nmetadata = f\"CRAG, {model_tested}\""]
},
{
"cell_type": "markdown",
@@ -144,48 +124,7 @@
]
}
],
"source": [
"from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import SKLearnVectorStore\n",
"from langchain_nomic.embeddings import NomicEmbeddings # local\n",
"from langchain_openai import OpenAIEmbeddings # api\n",
"\n",
"# List of URLs to load documents from\n",
"urls = [\n",
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
"]\n",
"\n",
"# Load documents from the URLs\n",
"docs = [WebBaseLoader(url).load() for url in urls]\n",
"docs_list = [item for sublist in docs for item in sublist]\n",
"\n",
"# Initialize a text splitter with specified chunk size and overlap\n",
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
" chunk_size=250, chunk_overlap=0\n",
")\n",
"\n",
"# Split the documents into chunks\n",
"doc_splits = text_splitter.split_documents(docs_list)\n",
"\n",
"# Embedding\n",
"'''\n",
"embedding=NomicEmbeddings(\n",
" model=\"nomic-embed-text-v1.5\",\n",
" inference_mode=\"local\",\n",
")\n",
"'''\n",
"embedding = OpenAIEmbeddings()\n",
"\n",
"# Add the document chunks to the \"vector store\"\n",
"vectorstore = SKLearnVectorStore.from_documents(\n",
" documents=doc_splits,\n",
" embedding=embedding,\n",
")\n",
"retriever = vectorstore.as_retriever(k=4)"
]
"source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_nomic.embeddings import NomicEmbeddings # local\nfrom langchain_openai import OpenAIEmbeddings # api\n\n# List of URLs to load documents from\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load documents from the URLs\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Initialize a text splitter with specified chunk size and overlap\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\n\n# Split the documents into chunks\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Embedding\n'''\nembedding=NomicEmbeddings(\n model=\"nomic-embed-text-v1.5\",\n inference_mode=\"local\",\n)\n'''\nembedding = OpenAIEmbeddings()\n\n# Add the document chunks to the \"vector store\"\nvectorstore = SKLearnVectorStore.from_documents(\n documents=doc_splits,\n embedding=embedding,\n)\nretriever = vectorstore.as_retriever(k=4)"]
},
{
"attachments": {},
@@ -210,47 +149,7 @@
]
}
],
"source": [
"### Retrieval Grader\n",
"\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain_community.chat_models import ChatOllama\n",
"from langchain_core.output_parsers import JsonOutputParser\n",
"from langchain_mistralai.chat_models import ChatMistralAI\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
"\n",
"# Prompt\n",
"prompt = PromptTemplate(\n",
" template=\"\"\"You are a teacher grading a quiz. You will be given: \n",
" 1/ a QUESTION\n",
" 2/ A FACT provided by the student\n",
" \n",
" You are grading RELEVANCE RECALL:\n",
" A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n",
" A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n",
" 1 is the highest (best) score. 0 is the lowest score you can give. \n",
" \n",
" Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n",
" \n",
" Avoid simply stating the correct answer at the outset.\n",
" \n",
" Question: {question} \\n\n",
" Fact: \\n\\n {documents} \\n\\n\n",
" \n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n",
" Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n",
" \"\"\",\n",
" input_variables=[\"question\", \"documents\"],\n",
")\n",
"\n",
"retrieval_grader = prompt | llm | JsonOutputParser()\n",
"question = \"agent memory\"\n",
"docs = retriever.invoke(question)\n",
"doc_txt = docs[1].page_content\n",
"print(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))"
]
"source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_mistralai.chat_models import ChatMistralAI\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a teacher grading a quiz. You will be given: \n 1/ a QUESTION\n 2/ A FACT provided by the student\n \n You are grading RELEVANCE RECALL:\n A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n 1 is the highest (best) score. 0 is the lowest score you can give. \n \n Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n \n Avoid simply stating the correct answer at the outset.\n \n Question: {question} \\n\n Fact: \\n\\n {documents} \\n\\n\n \n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))"]
},
{
"cell_type": "code",
@@ -266,37 +165,7 @@
]
}
],
"source": [
"### Generate\n",
"\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"# Prompt\n",
"prompt = PromptTemplate(\n",
" template=\"\"\"You are an assistant for question-answering tasks. \n",
" \n",
" Use the following documents to answer the question. \n",
" \n",
" If you don't know the answer, just say that you don't know. \n",
" \n",
" Use three sentences maximum and keep the answer concise:\n",
" Question: {question} \n",
" Documents: {documents} \n",
" Answer: \n",
" \"\"\",\n",
" input_variables=[\"question\", \"documents\"],\n",
")\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
"# Run\n",
"generation = rag_chain.invoke({\"documents\": docs, \"question\": question})\n",
"print(generation)"
]
"source": ["### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are an assistant for question-answering tasks. \n \n Use the following documents to answer the question. \n \n If you don't know the answer, just say that you don't know. \n \n Use three sentences maximum and keep the answer concise:\n Question: {question} \n Documents: {documents} \n Answer: \n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"]
},
{
"cell_type": "code",
@@ -304,13 +173,7 @@
"id": "b36a2f36-bc5f-408d-a5e8-3fa203c233f6",
"metadata": {},
"outputs": [],
"source": [
"### Search\n",
"\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"web_search_tool = TavilySearchResults(k=3)"
]
"source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"]
},
{
"cell_type": "markdown",
@@ -339,177 +202,7 @@
"output_type": "display_data"
}
],
"source": [
"from typing import List\n",
"from typing_extensions import TypedDict\n",
"from IPython.display import Image, display\n",
"from langchain.schema import Document\n",
"from langgraph.graph import START, END, StateGraph\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" search: whether to add search\n",
" documents: list of documents\n",
" \"\"\"\n",
"\n",
" question: str\n",
" generation: str\n",
" search: str\n",
" documents: List[str]\n",
" steps: List[str]\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, documents, that contains retrieved documents\n",
" \"\"\"\n",
" question = state[\"question\"]\n",
" documents = retriever.invoke(question)\n",
" steps = state[\"steps\"]\n",
" steps.append(\"retrieve_documents\")\n",
" return {\"documents\": documents, \"question\": question, \"steps\": steps}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation, that contains LLM generation\n",
" \"\"\"\n",
"\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n",
" steps = state[\"steps\"]\n",
" steps.append(\"generate_answer\")\n",
" return {\n",
" \"documents\": documents,\n",
" \"question\": question,\n",
" \"generation\": generation,\n",
" \"steps\": steps,\n",
" }\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with only filtered relevant documents\n",
" \"\"\"\n",
"\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" steps = state[\"steps\"]\n",
" steps.append(\"grade_document_retrieval\")\n",
" filtered_docs = []\n",
" search = \"No\"\n",
" for d in documents:\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"documents\": d.page_content}\n",
" )\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" filtered_docs.append(d)\n",
" else:\n",
" search = \"Yes\"\n",
" continue\n",
" return {\n",
" \"documents\": filtered_docs,\n",
" \"question\": question,\n",
" \"search\": search,\n",
" \"steps\": steps,\n",
" }\n",
"\n",
"\n",
"def web_search(state):\n",
" \"\"\"\n",
" Web search based on the re-phrased question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with appended web results\n",
" \"\"\"\n",
"\n",
" question = state[\"question\"]\n",
" documents = state.get(\"documents\", [])\n",
" steps = state[\"steps\"]\n",
" steps.append(\"web_search\")\n",
" web_results = web_search_tool.invoke({\"query\": question})\n",
" documents.extend(\n",
" [\n",
" Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n",
" for d in web_results\n",
" ]\n",
" )\n",
" return {\"documents\": documents, \"question\": question, \"steps\": steps}\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Binary decision for next node to call\n",
" \"\"\"\n",
" search = state[\"search\"]\n",
" if search == \"Yes\":\n",
" return \"search\"\n",
" else:\n",
" return \"generate\"\n",
"\n",
"\n",
"# Graph\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"web_search\", web_search) # web search\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
" decide_to_generate,\n",
" {\n",
" \"search\": \"web_search\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"web_search\", \"generate\")\n",
"workflow.add_edge(\"generate\", END)\n",
"\n",
"custom_graph = workflow.compile()\n",
"\n",
"display(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))"
]
"source": ["from typing import List\nfrom typing_extensions import TypedDict\nfrom IPython.display import Image, display\nfrom langchain.schema import Document\nfrom langgraph.graph import START, END, StateGraph\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n search: str\n documents: List[str]\n steps: List[str]\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n question = state[\"question\"]\n documents = retriever.invoke(question)\n steps = state[\"steps\"]\n steps.append(\"retrieve_documents\")\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n steps = state[\"steps\"]\n steps.append(\"generate_answer\")\n return {\n \"documents\": documents,\n \"question\": question,\n \"generation\": generation,\n \"steps\": steps,\n }\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n steps = state[\"steps\"]\n steps.append(\"grade_document_retrieval\")\n filtered_docs = []\n search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"documents\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n filtered_docs.append(d)\n else:\n search = \"Yes\"\n continue\n return {\n \"documents\": filtered_docs,\n \"question\": question,\n \"search\": search,\n \"steps\": steps,\n }\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n question = state[\"question\"]\n documents = state.get(\"documents\", [])\n steps = state[\"steps\"]\n steps.append(\"web_search\")\n web_results = web_search_tool.invoke({\"query\": question})\n documents.extend(\n [\n Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n for d in web_results\n ]\n )\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n search = state[\"search\"]\n if search == \"Yes\":\n return \"search\"\n else:\n return \"generate\"\n\n\n# Graph\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"web_search\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\ncustom_graph = workflow.compile()\n\ndisplay(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -532,21 +225,7 @@
"output_type": "execute_result"
}
],
"source": [
"import uuid\n",
"\n",
"def predict_custom_agent_local_answer(example: dict):\n",
" config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
" state_dict = custom_graph.invoke(\n",
" {\"question\": example[\"input\"], \"steps\": []}, config\n",
" )\n",
" return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n",
"\n",
"\n",
"example = {\"input\": \"What are the types of agent memory?\"}\n",
"response = predict_custom_agent_local_answer(example)\n",
"response"
]
"source": ["import uuid\n\ndef predict_custom_agent_local_answer(example: dict):\n config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n state_dict = custom_graph.invoke(\n {\"question\": example[\"input\"], \"steps\": []}, config\n )\n return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n\n\nexample = {\"input\": \"What are the types of agent memory?\"}\nresponse = predict_custom_agent_local_answer(example)\nresponse"]
},
{
"cell_type": "markdown",
@@ -582,41 +261,7 @@
"id": "b83706ac-724b-46b1-9f08-66e6c4fac742",
"metadata": {},
"outputs": [],
"source": [
"from langsmith import Client\n",
"\n",
"client = Client()\n",
"\n",
"# Create a dataset\n",
"examples = [\n",
" (\n",
" \"How does the ReAct agent use self-reflection? \",\n",
" \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n",
" ),\n",
" (\n",
" \"What are the types of biases that can arise with few-shot prompting?\",\n",
" \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n",
" ),\n",
" (\n",
" \"What are five types of adversarial attacks?\",\n",
" \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n",
" ),\n",
" (\n",
" \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n",
" \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n",
" ),\n",
" (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n",
"]\n",
"\n",
"# Save it\n",
"dataset_name = \"Corrective RAG Agent Testing\"\n",
"if not client.has_dataset(dataset_name=dataset_name):\n",
" dataset = client.create_dataset(dataset_name=dataset_name)\n",
" inputs, outputs = zip(\n",
" *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n",
" )\n",
" client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)"
]
"source": ["from langsmith import Client\n\nclient = Client()\n\n# Create a dataset\nexamples = [\n (\n \"How does the ReAct agent use self-reflection? \",\n \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n ),\n (\n \"What are the types of biases that can arise with few-shot prompting?\",\n \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n ),\n (\n \"What are five types of adversarial attacks?\",\n \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n ),\n (\n \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n ),\n (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n]\n\n# Save it\ndataset_name = \"Corrective RAG Agent Testing\"\nif not client.has_dataset(dataset_name=dataset_name):\n dataset = client.create_dataset(dataset_name=dataset_name)\n inputs, outputs = zip(\n *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n )\n client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)"]
},
{
"cell_type": "markdown",
@@ -636,38 +281,7 @@
"id": "0a63776c-f9cd-46ce-b8cf-95c066dc5b06",
"metadata": {},
"outputs": [],
"source": [
"from langchain import hub\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"# Grade prompt\n",
"grade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n",
"\n",
"def answer_evaluator(run, example) -> dict:\n",
" \"\"\"\n",
" A simple evaluator for RAG answer accuracy\n",
" \"\"\"\n",
"\n",
" # Get the question, the ground truth reference answer, RAG chain answer prediction\n",
" input_question = example.inputs[\"input\"]\n",
" reference = example.outputs[\"output\"]\n",
" prediction = run.outputs[\"response\"]\n",
"\n",
" # Define an LLM grader\n",
" llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
" answer_grader = grade_prompt_answer_accuracy | llm\n",
"\n",
" # Run evaluator\n",
" score = answer_grader.invoke(\n",
" {\n",
" \"question\": input_question,\n",
" \"correct_answer\": reference,\n",
" \"student_answer\": prediction,\n",
" }\n",
" )\n",
" score = score[\"Score\"]\n",
" return {\"key\": \"answer_v_reference_score\", \"score\": score}"
]
"source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\n# Grade prompt\ngrade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n\ndef answer_evaluator(run, example) -> dict:\n \"\"\"\n A simple evaluator for RAG answer accuracy\n \"\"\"\n\n # Get the question, the ground truth reference answer, RAG chain answer prediction\n input_question = example.inputs[\"input\"]\n reference = example.outputs[\"output\"]\n prediction = run.outputs[\"response\"]\n\n # Define an LLM grader\n llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n answer_grader = grade_prompt_answer_accuracy | llm\n\n # Run evaluator\n score = answer_grader.invoke(\n {\n \"question\": input_question,\n \"correct_answer\": reference,\n \"student_answer\": prediction,\n }\n )\n score = score[\"Score\"]\n return {\"key\": \"answer_v_reference_score\", \"score\": score}"]
},
{
"cell_type": "markdown",
@@ -687,50 +301,7 @@
"id": "deb28175-27a1-4afc-9747-2983e87fc881",
"metadata": {},
"outputs": [],
"source": [
"from langsmith.schemas import Example, Run\n",
"\n",
"# Reasoning traces that we expect the agents to take\n",
"expected_trajectory_1 = [\n",
" \"retrieve_documents\",\n",
" \"grade_document_retrieval\",\n",
" \"web_search\",\n",
" \"generate_answer\",\n",
"]\n",
"expected_trajectory_2 = [\n",
" \"retrieve_documents\",\n",
" \"grade_document_retrieval\",\n",
" \"generate_answer\",\n",
"]\n",
"\n",
"def check_trajectory_react(root_run: Run, example: Example) -> dict:\n",
" \"\"\"\n",
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
" \"\"\"\n",
" messages = root_run.outputs[\"messages\"]\n",
" tool_calls = find_tool_calls_react(messages)\n",
" print(f\"Tool calls ReAct agent: {tool_calls}\")\n",
" if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n",
" score = 1\n",
" else:\n",
" score = 0\n",
"\n",
" return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n",
"\n",
"\n",
"def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n",
" \"\"\"\n",
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
" \"\"\"\n",
" tool_calls = root_run.outputs[\"steps\"]\n",
" print(f\"Tool calls custom agent: {tool_calls}\")\n",
" if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n",
" score = 1\n",
" else:\n",
" score = 0\n",
"\n",
" return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}"
]
"source": ["from langsmith.schemas import Example, Run\n\n# Reasoning traces that we expect the agents to take\nexpected_trajectory_1 = [\n \"retrieve_documents\",\n \"grade_document_retrieval\",\n \"web_search\",\n \"generate_answer\",\n]\nexpected_trajectory_2 = [\n \"retrieve_documents\",\n \"grade_document_retrieval\",\n \"generate_answer\",\n]\n\ndef check_trajectory_react(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n messages = root_run.outputs[\"messages\"]\n tool_calls = find_tool_calls_react(messages)\n print(f\"Tool calls ReAct agent: {tool_calls}\")\n if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n\n\ndef check_trajectory_custom(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n tool_calls = root_run.outputs[\"steps\"]\n print(f\"Tool calls custom agent: {tool_calls}\")\n if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}"]
},
{
"cell_type": "code",
@@ -784,20 +355,7 @@
]
}
],
"source": [
"from langsmith.evaluation import evaluate\n",
"\n",
"experiment_prefix = f\"custom-agent-{model_tested}\"\n",
"experiment_results = evaluate(\n",
" predict_custom_agent_local_answer,\n",
" data=dataset_name,\n",
" evaluators=[answer_evaluator, check_trajectory_custom],\n",
" experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n",
" num_repetitions=3,\n",
" max_concurrency=1, # Use when running locally\n",
" metadata={\"version\": metadata},\n",
")"
]
"source": ["from langsmith.evaluation import evaluate\n\nexperiment_prefix = f\"custom-agent-{model_tested}\"\nexperiment_results = evaluate(\n predict_custom_agent_local_answer,\n data=dataset_name,\n evaluators=[answer_evaluator, check_trajectory_custom],\n experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n num_repetitions=3,\n max_concurrency=1, # Use when running locally\n metadata={\"version\": metadata},\n)"]
},
{
"attachments": {
@@ -824,7 +382,7 @@
"id": "79295798-0181-417e-abad-11dddb6ff05e",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
+15 -435
View File
@@ -59,9 +59,7 @@
"id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9",
"metadata": {},
"outputs": [],
"source": [
"! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph"
]
"source": ["! pip install -U langchain_community tiktoken langchain-openai langchainhub chromadb langchain langgraph"]
},
{
"cell_type": "markdown",
@@ -77,11 +75,7 @@
"id": "f18b63c7-d0d3-41c1-ae6b-5a0f1b8ccf0f",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"OPENAI_API_KEY\"] = \"<your-api-key>\""
]
"source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "markdown",
@@ -99,11 +93,7 @@
"id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "markdown",
@@ -121,34 +111,7 @@
"id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d",
"metadata": {},
"outputs": [],
"source": [
"from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import Chroma\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"urls = [\n",
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
"]\n",
"\n",
"docs = [WebBaseLoader(url).load() for url in urls]\n",
"docs_list = [item for sublist in docs for item in sublist]\n",
"\n",
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
" chunk_size=250, chunk_overlap=0\n",
")\n",
"doc_splits = text_splitter.split_documents(docs_list)\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=OpenAIEmbeddings(),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
"source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"]
},
{
"cell_type": "markdown",
@@ -180,46 +143,7 @@
]
}
],
"source": [
"### Retrieval Grader\n",
"\n",
"\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"\n",
"# Data model\n",
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
" If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n",
"grade_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n",
" ]\n",
")\n",
"\n",
"retrieval_grader = grade_prompt | structured_llm_grader\n",
"question = \"agent memory\"\n",
"docs = retriever.get_relevant_documents(question)\n",
"doc_txt = docs[1].page_content\n",
"print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
]
"source": ["### Retrieval Grader\n\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
},
{
"cell_type": "code",
@@ -235,31 +159,7 @@
]
}
],
"source": [
"### Generate\n",
"\n",
"from langchain import hub\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"# Prompt\n",
"prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
"# Run\n",
"generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
"print(generation)"
]
"source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
},
{
"cell_type": "code",
@@ -278,36 +178,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Hallucination Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeHallucinations(BaseModel):\n",
" \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
"\n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n",
" Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n",
"hallucination_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n",
" ]\n",
")\n",
"\n",
"hallucination_grader = hallucination_prompt | structured_llm_grader\n",
"hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
]
"source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -326,36 +197,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Answer Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeAnswer(BaseModel):\n",
" \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Answer addresses the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
"\n",
"# Prompt\n",
"system = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n",
" Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n",
"answer_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n",
" ]\n",
")\n",
"\n",
"answer_grader = answer_prompt | structured_llm_grader\n",
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
"source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -374,28 +216,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Question Re-writer\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt\n",
"system = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\n",
"re_write_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", system),\n",
" (\n",
" \"human\",\n",
" \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n",
" ),\n",
" ]\n",
")\n",
"\n",
"question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
"question_rewriter.invoke({\"question\": question})"
]
"source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"]
},
{
"cell_type": "markdown",
@@ -415,26 +236,7 @@
"id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents\n",
" \"\"\"\n",
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
"source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
},
{
"cell_type": "code",
@@ -442,167 +244,7 @@
"id": "add509d8-6682-4127-8d95-13dd37d79702",
"metadata": {},
"outputs": [],
"source": [
"### Nodes\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, documents, that contains retrieved documents\n",
" \"\"\"\n",
" print(\"---RETRIEVE---\")\n",
" question = state[\"question\"]\n",
"\n",
" # Retrieval\n",
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation, that contains LLM generation\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with only filtered relevant documents\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
" else:\n",
" print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates question key with a re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Binary decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---ASSESS GRADED DOCUMENTS---\")\n",
" state[\"question\"]\n",
" filtered_documents = state[\"documents\"]\n",
"\n",
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"def grade_generation_v_documents_and_question(state):\n",
" \"\"\"\n",
" Determines whether the generation is grounded in the document and answers question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK HALLUCINATIONS---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score.binary_score\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
" # Check question-answering\n",
" print(\"---GRADE GENERATION vs QUESTION---\")\n",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
" else:\n",
" print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
" return \"not useful\"\n",
" else:\n",
" pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
" return \"not supported\""
]
"source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
},
{
"cell_type": "markdown",
@@ -620,42 +262,7 @@
"id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
" decide_to_generate,\n",
" {\n",
" \"transform_query\": \"transform_query\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"transform_query\", \"retrieve\")\n",
"workflow.add_conditional_edges(\n",
" \"generate\",\n",
" grade_generation_v_documents_and_question,\n",
" {\n",
" \"not supported\": \"generate\",\n",
" \"useful\": END,\n",
" \"not useful\": \"transform_query\",\n",
" },\n",
")\n",
"\n",
"# Compile\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -694,22 +301,7 @@
]
}
],
"source": [
"from pprint import pprint\n",
"\n",
"# Run\n",
"inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" # Optional: print full state at each node\n",
" # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Explain how the different types of agent memory work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "code",
@@ -749,19 +341,7 @@
]
}
],
"source": [
"inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" # Optional: print full state at each node\n",
" # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["inputs = {\"question\": \"Explain how chain of thought prompting works?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "markdown",
@@ -781,7 +361,7 @@
"id": "42369ab8-322d-434a-b5dd-2266e4cb2903",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+14 -390
View File
@@ -59,10 +59,7 @@
"id": "d7f9cc6d-a70c-433a-b0ad-ea47c5a0717e",
"metadata": {},
"outputs": [],
"source": [
"%capture --no-stderr\n",
"%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]"
]
"source": ["%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph nomic[local]"]
},
{
"cell_type": "markdown",
@@ -94,10 +91,7 @@
"id": "bedffc73-6b10-42c8-8768-2085c8ed3398",
"metadata": {},
"outputs": [],
"source": [
"# Ollama model name\n",
"local_llm = \"mistral\""
]
"source": ["# Ollama model name\nlocal_llm = \"mistral\""]
},
{
"cell_type": "markdown",
@@ -115,13 +109,7 @@
"id": "2208f342-8163-4af3-8dc0-aa70f5e06143",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
]
"source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "markdown",
@@ -139,34 +127,7 @@
"id": "c3bb9060-ad74-4470-9991-2ba167b6b8d8",
"metadata": {},
"outputs": [],
"source": [
"from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
"from langchain_community.document_loaders import WebBaseLoader\n",
"from langchain_community.vectorstores import Chroma\n",
"from langchain_nomic.embeddings import NomicEmbeddings\n",
"\n",
"urls = [\n",
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
"]\n",
"\n",
"docs = [WebBaseLoader(url).load() for url in urls]\n",
"docs_list = [item for sublist in docs for item in sublist]\n",
"\n",
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
" chunk_size=250, chunk_overlap=0\n",
")\n",
"doc_splits = text_splitter.split_documents(docs_list)\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
"source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"]
},
{
"cell_type": "markdown",
@@ -190,33 +151,7 @@
]
}
],
"source": [
"### Retrieval Grader\n",
"\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain_community.chat_models import ChatOllama\n",
"from langchain_core.output_parsers import JsonOutputParser\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
"\n",
"prompt = PromptTemplate(\n",
" template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
" Here is the retrieved document: \\n\\n {document} \\n\\n\n",
" Here is the user question: {question} \\n\n",
" If the document contains keywords related to the user question, grade it as relevant. \\n\n",
" It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n",
" Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n",
" input_variables=[\"question\", \"document\"],\n",
")\n",
"\n",
"retrieval_grader = prompt | llm | JsonOutputParser()\n",
"question = \"agent memory\"\n",
"docs = retriever.get_relevant_documents(question)\n",
"doc_txt = docs[1].page_content\n",
"print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
]
"source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
},
{
"cell_type": "code",
@@ -232,31 +167,7 @@
]
}
],
"source": [
"### Generate\n",
"\n",
"from langchain import hub\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"# Prompt\n",
"prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"\n",
"# Post-processing\n",
"def format_docs(docs):\n",
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
"\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
"# Run\n",
"generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
"print(generation)"
]
"source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
},
{
"cell_type": "code",
@@ -275,28 +186,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Hallucination Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
"\n",
"# Prompt\n",
"prompt = PromptTemplate(\n",
" template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n",
" Here are the facts:\n",
" \\n ------- \\n\n",
" {documents} \n",
" \\n ------- \\n\n",
" Here is the answer: {generation}\n",
" Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n",
" Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n",
" input_variables=[\"generation\", \"documents\"],\n",
")\n",
"\n",
"hallucination_grader = prompt | llm | JsonOutputParser()\n",
"hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
]
"source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -315,28 +205,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Answer Grader\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n",
"\n",
"# Prompt\n",
"prompt = PromptTemplate(\n",
" template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n",
" Here is the answer:\n",
" \\n ------- \\n\n",
" {generation} \n",
" \\n ------- \\n\n",
" Here is the question: {question}\n",
" Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n",
" Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n",
" input_variables=[\"generation\", \"question\"],\n",
")\n",
"\n",
"answer_grader = prompt | llm | JsonOutputParser()\n",
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
"source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -355,23 +224,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Question Re-writer\n",
"\n",
"# LLM\n",
"llm = ChatOllama(model=local_llm, temperature=0)\n",
"\n",
"# Prompt\n",
"re_write_prompt = PromptTemplate(\n",
" template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
" for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
" Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n",
" input_variables=[\"generation\", \"question\"],\n",
")\n",
"\n",
"question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
"question_rewriter.invoke({\"question\": question})"
]
"source": ["### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"]
},
{
"cell_type": "markdown",
@@ -391,26 +244,7 @@
"id": "90fb1dc6-c482-483a-8441-39965c401beb",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents\n",
" \"\"\"\n",
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
"source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
},
{
"cell_type": "code",
@@ -418,167 +252,7 @@
"id": "5324ea49-5745-47b5-a0a5-bf58c8babe46",
"metadata": {},
"outputs": [],
"source": [
"### Nodes\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, documents, that contains retrieved documents\n",
" \"\"\"\n",
" print(\"---RETRIEVE---\")\n",
" question = state[\"question\"]\n",
"\n",
" # Retrieval\n",
" documents = retriever.get_relevant_documents(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation, that contains LLM generation\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with only filtered relevant documents\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
" else:\n",
" print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates question key with a re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}\n",
"\n",
"\n",
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Binary decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---ASSESS GRADED DOCUMENTS---\")\n",
" state[\"question\"]\n",
" filtered_documents = state[\"documents\"]\n",
"\n",
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"def grade_generation_v_documents_and_question(state):\n",
" \"\"\"\n",
" Determines whether the generation is grounded in the document and answers question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK HALLUCINATIONS---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score[\"score\"]\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
" # Check question-answering\n",
" print(\"---GRADE GENERATION vs QUESTION---\")\n",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score[\"score\"]\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
" else:\n",
" print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
" return \"not useful\"\n",
" else:\n",
" pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
" return \"not supported\""
]
"source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\n### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
},
{
"cell_type": "markdown",
@@ -596,42 +270,7 @@
"id": "5605dee4-b2df-46ae-a640-cc2ed90c21a6",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
" decide_to_generate,\n",
" {\n",
" \"transform_query\": \"transform_query\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"transform_query\", \"retrieve\")\n",
"workflow.add_conditional_edges(\n",
" \"generate\",\n",
" grade_generation_v_documents_and_question,\n",
" {\n",
" \"not supported\": \"generate\",\n",
" \"useful\": END,\n",
" \"not useful\": \"transform_query\",\n",
" },\n",
")\n",
"\n",
"# Compile\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
},
{
"cell_type": "markdown",
@@ -685,22 +324,7 @@
]
}
],
"source": [
"from pprint import pprint\n",
"\n",
"# Run\n",
"inputs = {\"question\": \"Explain how the different types of agent memory work?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" # Optional: print full state at each node\n",
" # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Explain how the different types of agent memory work?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "markdown",
@@ -718,7 +342,7 @@
"id": "953143c2-2f2a-4361-a36b-87db7cf21d63",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
@@ -33,9 +33,7 @@
"id": "a384cc48-0425-4e8f-aafc-cfb8e56025c9",
"metadata": {},
"outputs": [],
"source": [
"%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"
]
"source": ["%pip install -qU langchain-pinecone langchain-openai langchainhub langgraph"]
},
{
"cell_type": "markdown",
@@ -53,13 +51,7 @@
"id": "ccc3dae5-1df6-48ca-af8a-50f0e6128876",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\n",
"os.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""
]
"source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"<your-api-key>\""]
},
{
"cell_type": "code",
@@ -67,11 +59,7 @@
"id": "88637820",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""
]
"source": ["import os\n\nos.environ[\"LANGCHAIN_PROJECT\"] = \"pinecone-devconnect\""]
},
{
"cell_type": "markdown",
@@ -89,20 +77,7 @@
"id": "565a6d44-2c9f-4fff-b1ec-eea05df9350d",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import OpenAIEmbeddings\n",
"from langchain_pinecone import PineconeVectorStore\n",
"\n",
"# use pinecone movies database\n",
"\n",
"# Add to vectorDB\n",
"vectorstore = PineconeVectorStore(\n",
" embedding=OpenAIEmbeddings(),\n",
" index_name=\"sample-movies\",\n",
" text_key=\"summary\",\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
"source": ["from langchain_openai import OpenAIEmbeddings\nfrom langchain_pinecone import PineconeVectorStore\n\n# use pinecone movies database\n\n# Add to vectorDB\nvectorstore = PineconeVectorStore(\n embedding=OpenAIEmbeddings(),\n index_name=\"sample-movies\",\n text_key=\"summary\",\n)\nretriever = vectorstore.as_retriever()"]
},
{
"cell_type": "code",
@@ -129,13 +104,7 @@
]
}
],
"source": [
"docs = retriever.invoke(\"James Cameron\")\n",
"for doc in docs:\n",
" print(\"# \" + doc.metadata[\"title\"])\n",
" print(doc.page_content)\n",
" print()"
]
"source": ["docs = retriever.invoke(\"James Cameron\")\nfor doc in docs:\n print(\"# \" + doc.metadata[\"title\"])\n print(doc.page_content)\n print()"]
},
{
"cell_type": "markdown",
@@ -151,32 +120,7 @@
"id": "1fafad21-60cc-483e-92a3-6a7edb1838e3",
"metadata": {},
"outputs": [],
"source": [
"### Retrieval Grader\n",
"\n",
"from langchain import hub\n",
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"\n",
"# Data model\n",
"class GradeDocuments(BaseModel):\n",
" \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Documents are relevant to the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\n",
"grade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeDocuments)\n",
"\n",
"retrieval_grader = grade_prompt | structured_llm_grader"
]
"source": ["### Retrieval Grader\n\nfrom langchain import hub\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# https://smith.langchain.com/hub/efriis/self-rag-retrieval-grader\ngrade_prompt = hub.pull(\"efriis/self-rag-retrieval-grader\")\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\nretrieval_grader = grade_prompt | structured_llm_grader"]
},
{
"cell_type": "code",
@@ -193,14 +137,7 @@
]
}
],
"source": [
"# Test the retrieval grader\n",
"question = \"movies starring jason momoa\"\n",
"docs = retriever.invoke(question)\n",
"doc_txt = docs[0].page_content\n",
"print(doc_txt)\n",
"print(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
]
"source": ["# Test the retrieval grader\nquestion = \"movies starring jason momoa\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[0].page_content\nprint(doc_txt)\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
},
{
"cell_type": "markdown",
@@ -226,25 +163,7 @@
]
}
],
"source": [
"### Generate\n",
"\n",
"from langchain import hub\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"\n",
"# Prompt\n",
"prompt = hub.pull(\"rlm/rag-prompt\")\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"# Chain\n",
"rag_chain = prompt | llm | StrOutputParser()\n",
"\n",
"# Run\n",
"generation = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
"print(generation)"
]
"source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
},
{
"cell_type": "code",
@@ -270,30 +189,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Hallucination Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeHallucinations(BaseModel):\n",
" \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeHallucinations)\n",
"\n",
"# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\n",
"hallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n",
"\n",
"hallucination_grader = hallucination_prompt | structured_llm_grader\n",
"print(generation)\n",
"hallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
]
"source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# https://smith.langchain.com/hub/efriis/self-rag-hallucination-grader\nhallucination_prompt = hub.pull(\"efriis/self-rag-hallucination-grader\")\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nprint(generation)\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -320,31 +216,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Answer Grader\n",
"\n",
"\n",
"# Data model\n",
"class GradeAnswer(BaseModel):\n",
" \"\"\"Binary score to assess answer addresses question.\"\"\"\n",
"\n",
" binary_score: str = Field(\n",
" description=\"Answer addresses the question, 'yes' or 'no'\"\n",
" )\n",
"\n",
"\n",
"# LLM with function call\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"structured_llm_grader = llm.with_structured_output(GradeAnswer)\n",
"\n",
"# Prompt\n",
"answer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n",
"\n",
"answer_grader = answer_prompt | structured_llm_grader\n",
"print(question)\n",
"print(generation)\n",
"answer_grader.invoke({\"question\": question, \"generation\": generation})"
]
"source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nanswer_prompt = hub.pull(\"efriis/self-rag-answer-grader\")\n\nanswer_grader = answer_prompt | structured_llm_grader\nprint(question)\nprint(generation)\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
},
{
"cell_type": "code",
@@ -370,19 +242,7 @@
"output_type": "execute_result"
}
],
"source": [
"### Question Re-writer\n",
"\n",
"# LLM\n",
"llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n",
"\n",
"# Prompt\n",
"re_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n",
"\n",
"question_rewriter = re_write_prompt | llm | StrOutputParser()\n",
"print(question)\n",
"question_rewriter.invoke({\"question\": question})"
]
"source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nre_write_prompt = hub.pull(\"efriis/self-rag-question-rewriter\")\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nprint(question)\nquestion_rewriter.invoke({\"question\": question})"]
},
{
"cell_type": "markdown",
@@ -402,26 +262,7 @@
"id": "f1617e9e-66a8-4c1a-a1fe-cc936284c085",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"\n",
"class GraphState(TypedDict):\n",
" \"\"\"\n",
" Represents the state of our graph.\n",
"\n",
" Attributes:\n",
" question: question\n",
" generation: LLM generation\n",
" documents: list of documents\n",
" \"\"\"\n",
"\n",
" question: str\n",
" generation: str\n",
" documents: List[str]"
]
"source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
},
{
"cell_type": "code",
@@ -429,97 +270,7 @@
"id": "add509d8-6682-4127-8d95-13dd37d79702",
"metadata": {},
"outputs": [],
"source": [
"### Nodes\n",
"\n",
"\n",
"def retrieve(state):\n",
" \"\"\"\n",
" Retrieve documents\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, documents, that contains retrieved documents\n",
" \"\"\"\n",
" print(\"---RETRIEVE---\")\n",
" question = state[\"question\"]\n",
"\n",
" # Retrieval\n",
" documents = retriever.invoke(question)\n",
" return {\"documents\": documents, \"question\": question}\n",
"\n",
"\n",
"def generate(state):\n",
" \"\"\"\n",
" Generate answer\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): New key added to state, generation, that contains LLM generation\n",
" \"\"\"\n",
" print(\"---GENERATE---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # RAG generation\n",
" generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n",
" return {\"documents\": documents, \"question\": question, \"generation\": generation}\n",
"\n",
"\n",
"def grade_documents(state):\n",
" \"\"\"\n",
" Determines whether the retrieved documents are relevant to the question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates documents key with only filtered relevant documents\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Score each doc\n",
" filtered_docs = []\n",
" for d in documents:\n",
" score = retrieval_grader.invoke(\n",
" {\"question\": question, \"document\": d.page_content}\n",
" )\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
" filtered_docs.append(d)\n",
" else:\n",
" print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
" continue\n",
" return {\"documents\": filtered_docs, \"question\": question}\n",
"\n",
"\n",
"def transform_query(state):\n",
" \"\"\"\n",
" Transform the query to produce a better question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" state (dict): Updates question key with a re-phrased question\n",
" \"\"\"\n",
"\n",
" print(\"---TRANSFORM QUERY---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
"\n",
" # Re-write question\n",
" better_question = question_rewriter.invoke({\"question\": question})\n",
" return {\"documents\": documents, \"question\": better_question}"
]
"source": ["### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}"]
},
{
"cell_type": "code",
@@ -527,76 +278,7 @@
"id": "09fc91b4",
"metadata": {},
"outputs": [],
"source": [
"### Edges\n",
"\n",
"\n",
"def decide_to_generate(state):\n",
" \"\"\"\n",
" Determines whether to generate an answer, or re-generate a question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Binary decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---ASSESS GRADED DOCUMENTS---\")\n",
" state[\"question\"]\n",
" filtered_documents = state[\"documents\"]\n",
"\n",
" if not filtered_documents:\n",
" # All documents have been filtered check_relevance\n",
" # We will re-generate a new query\n",
" print(\n",
" \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n",
" )\n",
" return \"transform_query\"\n",
" else:\n",
" # We have relevant documents, so generate answer\n",
" print(\"---DECISION: GENERATE---\")\n",
" return \"generate\"\n",
"\n",
"\n",
"def grade_generation_v_documents_and_question(state):\n",
" \"\"\"\n",
" Determines whether the generation is grounded in the document and answers question.\n",
"\n",
" Args:\n",
" state (dict): The current graph state\n",
"\n",
" Returns:\n",
" str: Decision for next node to call\n",
" \"\"\"\n",
"\n",
" print(\"---CHECK HALLUCINATIONS---\")\n",
" question = state[\"question\"]\n",
" documents = state[\"documents\"]\n",
" generation = state[\"generation\"]\n",
"\n",
" score = hallucination_grader.invoke(\n",
" {\"documents\": documents, \"generation\": generation}\n",
" )\n",
" grade = score.binary_score\n",
"\n",
" # Check hallucination\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n",
" # Check question-answering\n",
" print(\"---GRADE GENERATION vs QUESTION---\")\n",
" score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n",
" grade = score.binary_score\n",
" if grade == \"yes\":\n",
" print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n",
" return \"useful\"\n",
" else:\n",
" print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n",
" return \"not useful\"\n",
" else:\n",
" pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n",
" return \"not supported\""
]
"source": ["### Edges\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
},
{
"cell_type": "markdown",
@@ -614,42 +296,7 @@
"id": "0e09ca9f-e36d-4ef4-a0d5-79fdbada9fe0",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"workflow = StateGraph(GraphState)\n",
"\n",
"# Define the nodes\n",
"workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
"workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
"workflow.add_node(\"generate\", generate) # generatae\n",
"workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
"\n",
"# Build graph\n",
"workflow.set_entry_point(\"retrieve\")\n",
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
"workflow.add_conditional_edges(\n",
" \"grade_documents\",\n",
" decide_to_generate,\n",
" {\n",
" \"transform_query\": \"transform_query\",\n",
" \"generate\": \"generate\",\n",
" },\n",
")\n",
"workflow.add_edge(\"transform_query\", \"retrieve\")\n",
"workflow.add_conditional_edges(\n",
" \"generate\",\n",
" grade_generation_v_documents_and_question,\n",
" {\n",
" \"not supported\": \"generate\",\n",
" \"useful\": END,\n",
" \"not useful\": \"transform_query\",\n",
" },\n",
")\n",
"\n",
"# Compile\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -684,20 +331,7 @@
]
}
],
"source": [
"from pprint import pprint\n",
"\n",
"# Run\n",
"inputs = {\"question\": \"Movies that star Daniel Craig\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"Movies that star Daniel Craig\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "code",
@@ -705,17 +339,7 @@
"id": "4138bc51-8c84-4b8a-8d24-f7f470721f6f",
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"question\": \"Which movies are about aliens?\"}\n",
"for output in app.stream(inputs):\n",
" for key, value in output.items():\n",
" # Node\n",
" pprint(f\"Node '{key}':\")\n",
" pprint(\"\\n---\\n\")\n",
"\n",
"# Final generation\n",
"pprint(value[\"generation\"])"
]
"source": ["inputs = {\"question\": \"Which movies are about aliens?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
},
{
"cell_type": "code",
@@ -723,7 +347,7 @@
"id": "42369ab8-322d-434a-b5dd-2266e4cb2903",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+11 -132
View File
@@ -32,10 +32,7 @@
"id": "8b323f43-328b-4b4b-88b0-6c84dc0a1d60",
"metadata": {},
"outputs": [],
"source": [
"%pip install -U --quiet langgraph langchain-fireworks\n",
"%pip install -U --quiet tavily-python"
]
"source": ["%pip install -U --quiet langgraph langchain-fireworks\n%pip install -U --quiet tavily-python"]
},
{
"cell_type": "code",
@@ -43,24 +40,7 @@
"id": "3368f330-cad6-4d35-a291-68fbf4389d98",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str) -> None:\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"# Optional: Configure tracing to visualize and debug the agent\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflection\"\n",
"\n",
"_set_if_undefined(\"FIREWORKS_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflection\"\n\n_set_if_undefined(\"FIREWORKS_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -78,28 +58,7 @@
"id": "cc10028f-9cef-4936-9419-cbdf06d24f1e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_fireworks import ChatFireworks\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are an essay assistant tasked with writing excellent 5-paragraph essays.\"\n",
" \" Generate the best essay possible for the user's request.\"\n",
" \" If the user provides critique, respond with a revised version of your previous attempts.\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"llm = ChatFireworks(\n",
" model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n",
" model_kwargs={\"max_tokens\": 32768},\n",
")\n",
"generate = prompt | llm"
]
"source": ["from langchain_core.messages import AIMessage, BaseMessage, HumanMessage\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_fireworks import ChatFireworks\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an essay assistant tasked with writing excellent 5-paragraph essays.\"\n \" Generate the best essay possible for the user's request.\"\n \" If the user provides critique, respond with a revised version of your previous attempts.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\nllm = ChatFireworks(\n model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n model_kwargs={\"max_tokens\": 32768},\n)\ngenerate = prompt | llm"]
},
{
"cell_type": "code",
@@ -127,15 +86,7 @@
]
}
],
"source": [
"essay = \"\"\n",
"request = HumanMessage(\n",
" content=\"Write an essay on why the little prince is relevant in modern childhood\"\n",
")\n",
"for chunk in generate.stream({\"messages\": [request]}):\n",
" print(chunk.content, end=\"\")\n",
" essay += chunk.content"
]
"source": ["essay = \"\"\nrequest = HumanMessage(\n content=\"Write an essay on why the little prince is relevant in modern childhood\"\n)\nfor chunk in generate.stream({\"messages\": [request]}):\n print(chunk.content, end=\"\")\n essay += chunk.content"]
},
{
"cell_type": "markdown",
@@ -151,19 +102,7 @@
"id": "a705be92-88c0-4f4f-b4c2-cdcd9af8cb2c",
"metadata": {},
"outputs": [],
"source": [
"reflection_prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\"\n",
" \" Provide detailed recommendations, including requests for length, depth, style, etc.\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" ]\n",
")\n",
"reflect = reflection_prompt | llm"
]
"source": ["reflection_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\"\n \" Provide detailed recommendations, including requests for length, depth, style, etc.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\nreflect = reflection_prompt | llm"]
},
{
"cell_type": "code",
@@ -193,12 +132,7 @@
]
}
],
"source": [
"reflection = \"\"\n",
"for chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n",
" print(chunk.content, end=\"\")\n",
" reflection += chunk.content"
]
"source": ["reflection = \"\"\nfor chunk in reflect.stream({\"messages\": [request, HumanMessage(content=essay)]}):\n print(chunk.content, end=\"\")\n reflection += chunk.content"]
},
{
"cell_type": "markdown",
@@ -236,12 +170,7 @@
]
}
],
"source": [
"for chunk in generate.stream(\n",
" {\"messages\": [request, AIMessage(content=essay), HumanMessage(content=reflection)]}\n",
"):\n",
" print(chunk.content, end=\"\")"
]
"source": ["for chunk in generate.stream(\n {\"messages\": [request, AIMessage(content=essay), HumanMessage(content=reflection)]}\n):\n print(chunk.content, end=\"\")"]
},
{
"cell_type": "markdown",
@@ -259,45 +188,7 @@
"id": "9e9a9d7c-5d2e-4194-b745-4511ec20db76",
"metadata": {},
"outputs": [],
"source": [
"from typing import List, Sequence\n",
"\n",
"from langgraph.graph import END, MessageGraph\n",
"\n",
"\n",
"async def generation_node(state: Sequence[BaseMessage]):\n",
" return await generate.ainvoke({\"messages\": state})\n",
"\n",
"\n",
"async def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n",
" # Other messages we need to adjust\n",
" cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n",
" # First message is the original user request. We hold it the same for all nodes\n",
" translated = [messages[0]] + [\n",
" cls_map[msg.type](content=msg.content) for msg in messages[1:]\n",
" ]\n",
" res = await reflect.ainvoke({\"messages\": translated})\n",
" # We treat the output of this as human feedback for the generator\n",
" return HumanMessage(content=res.content)\n",
"\n",
"\n",
"builder = MessageGraph()\n",
"builder.add_node(\"generate\", generation_node)\n",
"builder.add_node(\"reflect\", reflection_node)\n",
"builder.set_entry_point(\"generate\")\n",
"\n",
"\n",
"def should_continue(state: List[BaseMessage]):\n",
" if len(state) > 6:\n",
" # End after 3 iterations\n",
" return END\n",
" return \"reflect\"\n",
"\n",
"\n",
"builder.add_conditional_edges(\"generate\", should_continue)\n",
"builder.add_edge(\"reflect\", \"generate\")\n",
"graph = builder.compile()"
]
"source": ["from typing import List, Sequence\n\nfrom langgraph.graph import END, MessageGraph, START\n\n\nasync def generation_node(state: Sequence[BaseMessage]):\n return await generate.ainvoke({\"messages\": state})\n\n\nasync def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n # Other messages we need to adjust\n cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n # First message is the original user request. We hold it the same for all nodes\n translated = [messages[0]] + [\n cls_map[msg.type](content=msg.content) for msg in messages[1:]\n ]\n res = await reflect.ainvoke({\"messages\": translated})\n # We treat the output of this as human feedback for the generator\n return HumanMessage(content=res.content)\n\n\nbuilder = MessageGraph()\nbuilder.add_node(\"generate\", generation_node)\nbuilder.add_node(\"reflect\", reflection_node)\nbuilder.add_edge(START, \"generate\")\n\n\ndef should_continue(state: List[BaseMessage]):\n if len(state) > 6:\n # End after 3 iterations\n return END\n return \"reflect\"\n\n\nbuilder.add_conditional_edges(\"generate\", should_continue)\nbuilder.add_edge(\"reflect\", \"generate\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -328,17 +219,7 @@
]
}
],
"source": [
"async for event in graph.astream(\n",
" [\n",
" HumanMessage(\n",
" content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n",
" )\n",
" ],\n",
"):\n",
" print(event)\n",
" print(\"---\")"
]
"source": ["async for event in graph.astream(\n [\n HumanMessage(\n content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n )\n ],\n):\n print(event)\n print(\"---\")"]
},
{
"cell_type": "code",
@@ -490,9 +371,7 @@
]
}
],
"source": [
"ChatPromptTemplate.from_messages(event[END]).pretty_print()"
]
"source": ["ChatPromptTemplate.from_messages(event[END]).pretty_print()"]
},
{
"cell_type": "markdown",
@@ -510,7 +389,7 @@
"id": "7c0e3efd-7f54-410e-bd31-36185a46b9a8",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+13 -254
View File
@@ -40,10 +40,7 @@
"id": "1b64a6f6-1d32-48be-92b5-66c3b04b17f7",
"metadata": {},
"outputs": [],
"source": [
"%pip install -U --quiet langgraph langchain_anthropic\n",
"%pip install -U --quiet tavily-python"
]
"source": ["%pip install -U --quiet langgraph langchain_anthropic\n%pip install -U --quiet tavily-python"]
},
{
"cell_type": "code",
@@ -51,25 +48,7 @@
"id": "a917bb70-f84c-48e6-8d32-d14f9df2ca2f",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str) -> None:\n",
" if os.environ.get(var):\n",
" return\n",
" os.environ[var] = getpass.getpass(var)\n",
"\n",
"\n",
"# Optional: Configure tracing to visualize and debug the agent\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n",
"\n",
"_set_if_undefined(\"ANTHROPIC_API_KEY\")\n",
"_set_if_undefined(\"TAVILY_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n\n_set_if_undefined(\"ANTHROPIC_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"]
},
{
"cell_type": "code",
@@ -77,15 +56,7 @@
"id": "567b6c4a",
"metadata": {},
"outputs": [],
"source": [
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n",
"# You could also use OpenAI or another provider\n",
"# from langchain_openai import ChatOpenAI\n",
"\n",
"# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
]
"source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n# You could also use OpenAI or another provider\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"]
},
{
"cell_type": "markdown",
@@ -110,13 +81,7 @@
"id": "5a2ac853-b8a6-40de-b7fe-3f9f3c5ca4d2",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n",
"\n",
"search = TavilySearchAPIWrapper()\n",
"tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"]
},
{
"cell_type": "markdown",
@@ -132,54 +97,7 @@
"id": "5fffa8d5-068a-4f0b-adfc-b4daf30ef294",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import HumanMessage, ToolMessage\n",
"from langchain_core.output_parsers.openai_tools import PydanticToolsParser\n",
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
"from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n",
"\n",
"\n",
"class Reflection(BaseModel):\n",
" missing: str = Field(description=\"Critique of what is missing.\")\n",
" superfluous: str = Field(description=\"Critique of what is superfluous\")\n",
"\n",
"\n",
"class AnswerQuestion(BaseModel):\n",
" \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n",
"\n",
" answer: str = Field(description=\"~250 word detailed answer to the question.\")\n",
" reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n",
" search_queries: list[str] = Field(\n",
" description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n",
" )\n",
"\n",
"\n",
"class ResponderWithRetries:\n",
" def __init__(self, runnable, validator):\n",
" self.runnable = runnable\n",
" self.validator = validator\n",
"\n",
" def respond(self, state: list):\n",
" response = []\n",
" for attempt in range(3):\n",
" response = self.runnable.invoke(\n",
" {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n",
" )\n",
" try:\n",
" self.validator.invoke(response)\n",
" return response\n",
" except ValidationError as e:\n",
" state = state + [\n",
" response,\n",
" ToolMessage(\n",
" content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n",
" + self.validator.schema_json()\n",
" + \" Respond by fixing all validation errors.\",\n",
" tool_call_id=response.tool_calls[0][\"id\"],\n",
" ),\n",
" ]\n",
" return response"
]
"source": ["from langchain_core.messages import HumanMessage, ToolMessage\nfrom langchain_core.output_parsers.openai_tools import PydanticToolsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n\n\nclass Reflection(BaseModel):\n missing: str = Field(description=\"Critique of what is missing.\")\n superfluous: str = Field(description=\"Critique of what is superfluous\")\n\n\nclass AnswerQuestion(BaseModel):\n \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n\n answer: str = Field(description=\"~250 word detailed answer to the question.\")\n reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n search_queries: list[str] = Field(\n description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n )\n\n\nclass ResponderWithRetries:\n def __init__(self, runnable, validator):\n self.runnable = runnable\n self.validator = validator\n\n def respond(self, state: list):\n response = []\n for attempt in range(3):\n response = self.runnable.invoke(\n {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n )\n try:\n self.validator.invoke(response)\n return response\n except ValidationError as e:\n state = state + [\n response,\n ToolMessage(\n content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n + self.validator.schema_json()\n + \" Respond by fixing all validation errors.\",\n tool_call_id=response.tool_calls[0][\"id\"],\n ),\n ]\n return response"]
},
{
"cell_type": "code",
@@ -196,40 +114,7 @@
]
}
],
"source": [
"import datetime\n",
"\n",
"actor_prompt_template = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"system\",\n",
" \"\"\"You are expert researcher.\n",
"Current time: {time}\n",
"\n",
"1. {first_instruction}\n",
"2. Reflect and critique your answer. Be severe to maximize improvement.\n",
"3. Recommend search queries to research information and improve your answer.\"\"\",\n",
" ),\n",
" MessagesPlaceholder(variable_name=\"messages\"),\n",
" (\n",
" \"user\",\n",
" \"\\n\\n<system>Reflect on the user's original question and the\"\n",
" \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n",
" ),\n",
" ]\n",
").partial(\n",
" time=lambda: datetime.datetime.now().isoformat(),\n",
")\n",
"initial_answer_chain = actor_prompt_template.partial(\n",
" first_instruction=\"Provide a detailed ~250 word answer.\",\n",
" function_name=AnswerQuestion.__name__,\n",
") | llm.bind_tools(tools=[AnswerQuestion])\n",
"validator = PydanticToolsParser(tools=[AnswerQuestion])\n",
"\n",
"first_responder = ResponderWithRetries(\n",
" runnable=initial_answer_chain, validator=validator\n",
")"
]
"source": ["import datetime\n\nactor_prompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are expert researcher.\nCurrent time: {time}\n\n1. {first_instruction}\n2. Reflect and critique your answer. Be severe to maximize improvement.\n3. Recommend search queries to research information and improve your answer.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"user\",\n \"\\n\\n<system>Reflect on the user's original question and the\"\n \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n ),\n ]\n).partial(\n time=lambda: datetime.datetime.now().isoformat(),\n)\ninitial_answer_chain = actor_prompt_template.partial(\n first_instruction=\"Provide a detailed ~250 word answer.\",\n function_name=AnswerQuestion.__name__,\n) | llm.bind_tools(tools=[AnswerQuestion])\nvalidator = PydanticToolsParser(tools=[AnswerQuestion])\n\nfirst_responder = ResponderWithRetries(\n runnable=initial_answer_chain, validator=validator\n)"]
},
{
"cell_type": "code",
@@ -237,10 +122,7 @@
"id": "5922e1fe-7533-4f41-8b1d-d812707c1968",
"metadata": {},
"outputs": [],
"source": [
"example_question = \"Why is reflection useful in AI?\"\n",
"initial = first_responder.respond([HumanMessage(content=example_question)])"
]
"source": ["example_question = \"Why is reflection useful in AI?\"\ninitial = first_responder.respond([HumanMessage(content=example_question)])"]
},
{
"cell_type": "markdown",
@@ -258,38 +140,7 @@
"id": "2605fd8d-c663-446f-ba25-751190195749",
"metadata": {},
"outputs": [],
"source": [
"revise_instructions = \"\"\"Revise your previous answer using the new information.\n",
" - You should use the previous critique to add important information to your answer.\n",
" - You MUST include numerical citations in your revised answer to ensure it can be verified.\n",
" - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n",
" - [1] https://example.com\n",
" - [2] https://example.com\n",
" - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n",
"\"\"\"\n",
"\n",
"\n",
"# Extend the initial answer schema to include references.\n",
"# Forcing citation in the model encourages grounded responses\n",
"class ReviseAnswer(AnswerQuestion):\n",
" \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n",
"\n",
" cite your reflection with references, and finally\n",
" add search queries to improve the answer.\"\"\"\n",
"\n",
" references: list[str] = Field(\n",
" description=\"Citations motivating your updated answer.\"\n",
" )\n",
"\n",
"\n",
"revision_chain = actor_prompt_template.partial(\n",
" first_instruction=revise_instructions,\n",
" function_name=ReviseAnswer.__name__,\n",
") | llm.bind_tools(tools=[ReviseAnswer])\n",
"revision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n",
"\n",
"revisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"
]
"source": ["revise_instructions = \"\"\"Revise your previous answer using the new information.\n - You should use the previous critique to add important information to your answer.\n - You MUST include numerical citations in your revised answer to ensure it can be verified.\n - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n - [1] https://example.com\n - [2] https://example.com\n - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n\"\"\"\n\n\n# Extend the initial answer schema to include references.\n# Forcing citation in the model encourages grounded responses\nclass ReviseAnswer(AnswerQuestion):\n \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n\n cite your reflection with references, and finally\n add search queries to improve the answer.\"\"\"\n\n references: list[str] = Field(\n description=\"Citations motivating your updated answer.\"\n )\n\n\nrevision_chain = actor_prompt_template.partial(\n first_instruction=revise_instructions,\n function_name=ReviseAnswer.__name__,\n) | llm.bind_tools(tools=[ReviseAnswer])\nrevision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n\nrevisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"]
},
{
"cell_type": "code",
@@ -308,25 +159,7 @@
"output_type": "execute_result"
}
],
"source": [
"import json\n",
"\n",
"revised = revisor.respond(\n",
" [\n",
" HumanMessage(content=example_question),\n",
" initial,\n",
" ToolMessage(\n",
" tool_call_id=initial.tool_calls[0][\"id\"],\n",
" content=json.dumps(\n",
" tavily_tool.invoke(\n",
" {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n",
" )\n",
" ),\n",
" ),\n",
" ]\n",
")\n",
"revised"
]
"source": ["import json\n\nrevised = revisor.respond(\n [\n HumanMessage(content=example_question),\n initial,\n ToolMessage(\n tool_call_id=initial.tool_calls[0][\"id\"],\n content=json.dumps(\n tavily_tool.invoke(\n {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n )\n ),\n ),\n ]\n)\nrevised"]
},
{
"cell_type": "markdown",
@@ -344,24 +177,7 @@
"id": "fccd6a17",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import StructuredTool\n",
"\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"\n",
"def run_queries(search_queries: list[str], **kwargs):\n",
" \"\"\"Run the generated queries.\"\"\"\n",
" return tavily_tool.batch([{\"query\": query} for query in search_queries])\n",
"\n",
"\n",
"tool_node = ToolNode(\n",
" [\n",
" StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n",
" StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n",
" ]\n",
")"
]
"source": ["from langchain_core.tools import StructuredTool\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef run_queries(search_queries: list[str], **kwargs):\n \"\"\"Run the generated queries.\"\"\"\n return tavily_tool.batch([{\"query\": query} for query in search_queries])\n\n\ntool_node = ToolNode(\n [\n StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n ]\n)"]
},
{
"cell_type": "markdown",
@@ -380,48 +196,7 @@
"id": "3c57318f-a30c-4dbd-9b88-f2633e8cb3b1",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"from langgraph.graph import END, MessageGraph\n",
"\n",
"MAX_ITERATIONS = 5\n",
"builder = MessageGraph()\n",
"builder.add_node(\"draft\", first_responder.respond)\n",
"\n",
"\n",
"builder.add_node(\"execute_tools\", tool_node)\n",
"builder.add_node(\"revise\", revisor.respond)\n",
"# draft -> execute_tools\n",
"builder.add_edge(\"draft\", \"execute_tools\")\n",
"# execute_tools -> revise\n",
"builder.add_edge(\"execute_tools\", \"revise\")\n",
"\n",
"# Define looping logic:\n",
"\n",
"\n",
"def _get_num_iterations(state: list):\n",
" i = 0\n",
" for m in state[::-1]:\n",
" if m.type not in {\"tool\", \"ai\"}:\n",
" break\n",
" i += 1\n",
" return i\n",
"\n",
"\n",
"def event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n",
" # in our case, we'll just stop after N plans\n",
" num_iterations = _get_num_iterations(state)\n",
" if num_iterations > MAX_ITERATIONS:\n",
" return END\n",
" return \"execute_tools\"\n",
"\n",
"\n",
"# revise -> execute_tools OR end\n",
"builder.add_conditional_edges(\"revise\", event_loop)\n",
"builder.set_entry_point(\"draft\")\n",
"graph = builder.compile()"
]
"source": ["from typing import Literal\n\nfrom langgraph.graph import END, MessageGraph, START\n\nMAX_ITERATIONS = 5\nbuilder = MessageGraph()\nbuilder.add_node(\"draft\", first_responder.respond)\n\n\nbuilder.add_node(\"execute_tools\", tool_node)\nbuilder.add_node(\"revise\", revisor.respond)\n# draft -> execute_tools\nbuilder.add_edge(\"draft\", \"execute_tools\")\n# execute_tools -> revise\nbuilder.add_edge(\"execute_tools\", \"revise\")\n\n# Define looping logic:\n\n\ndef _get_num_iterations(state: list):\n i = 0\n for m in state[::-1]:\n if m.type not in {\"tool\", \"ai\"}:\n break\n i += 1\n return i\n\n\ndef event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n # in our case, we'll just stop after N plans\n num_iterations = _get_num_iterations(state)\n if num_iterations > MAX_ITERATIONS:\n return END\n return \"execute_tools\"\n\n\n# revise -> execute_tools OR end\nbuilder.add_conditional_edges(\"revise\", event_loop)\nbuilder.add_edge(START, \"draft\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -440,15 +215,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(graph.get_graph().draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "code",
@@ -563,15 +330,7 @@
]
}
],
"source": [
"events = graph.stream(\n",
" [HumanMessage(content=\"How should we handle the climate crisis?\")],\n",
" stream_mode=\"values\",\n",
")\n",
"for i, step in enumerate(events):\n",
" print(f\"Step {i}\")\n",
" step[-1].pretty_print()"
]
"source": ["events = graph.stream(\n [HumanMessage(content=\"How should we handle the climate crisis?\")],\n stream_mode=\"values\",\n)\nfor i, step in enumerate(events):\n print(f\"Step {i}\")\n step[-1].pretty_print()"]
},
{
"cell_type": "markdown",
+14 -155
View File
@@ -30,10 +30,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain-anthropic"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-anthropic"]
},
{
"cell_type": "markdown",
@@ -49,18 +46,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -76,10 +62,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -105,22 +88,7 @@
"id": "c9172aa0",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# Add messages essentially does this with more\n",
"# robust handling\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# Add messages essentially does this with more\n# robust handling\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
},
{
"cell_type": "markdown",
@@ -140,19 +108,7 @@
"id": "3a1c8796",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder, but don't tell the LLM that...\n",
" return [\"The weather will be sunny with a high of 27 C.\"]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder, but don't tell the LLM that...\n return [\"The weather will be sunny with a high of 27 C.\"]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -169,11 +125,7 @@
"id": "56681368",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -197,11 +149,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "markdown",
@@ -224,20 +172,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
"\n",
"\n",
"class Response(BaseModel):\n",
" \"\"\"Final response to the user\"\"\"\n",
"\n",
" temperature: float = Field(description=\"the temperature\")\n",
" other_notes: str = Field(description=\"any other notes about the weather\")\n",
"\n",
"\n",
"# Bind to the actual tools + the response format!\n",
"model = model.bind_tools(tools + [Response], tool_choice=\"any\")"
]
"source": ["from langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Response(BaseModel):\n \"\"\"Final response to the user\"\"\"\n\n temperature: float = Field(description=\"the temperature\")\n other_notes: str = Field(description=\"any other notes about the weather\")\n\n\n# Bind to the actual tools + the response format!\nmodel = model.bind_tools(tools + [Response], tool_choice=\"any\")"]
},
{
"cell_type": "markdown",
@@ -263,16 +198,7 @@
"id": "ea793afa-2eab-4901-910d-6eed90cd6564",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence, TypedDict\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"]
},
{
"cell_type": "markdown",
@@ -311,31 +237,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def route(state: AgentState) -> Literal[\"action\", \"__end__\"]:\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"__end__\"\n",
" # Otherwise if there is, we need to check what type of function call it is\n",
" if last_message.tool_calls[0][\"name\"] == Response.__name__:\n",
" return \"__end__\"\n",
" # Otherwise we continue\n",
" return \"action\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state: AgentState):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}"
]
"source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef route(state: AgentState) -> Literal[\"action\", \"__end__\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"__end__\"\n # Otherwise if there is, we need to check what type of function call it is\n if last_message.tool_calls[0][\"name\"] == Response.__name__:\n return \"__end__\"\n # Otherwise we continue\n return \"action\"\n\n\n# Define the function that calls the model\ndef call_model(state: AgentState):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}"]
},
{
"cell_type": "markdown",
@@ -353,38 +255,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" route,\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n route,\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -403,11 +274,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph(xray=True).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph(xray=True).draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -465,15 +332,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"for output in app.stream(inputs, stream_mode=\"values\"):\n",
" last_msg = output[\"messages\"][-1]\n",
" last_msg.pretty_print()\n",
" print(\"\\n---\\n\")"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor output in app.stream(inputs, stream_mode=\"values\"):\n last_msg = output[\"messages\"][-1]\n last_msg.pretty_print()\n print(\"\\n---\\n\")"]
},
{
"cell_type": "code",
@@ -481,7 +340,7 @@
"id": "eed4360d-2cdf-497b-b03f-8bc51062f780",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
+16 -178
View File
@@ -47,9 +47,7 @@
"id": "7f52bded-9d23-4826-8bfc-20b0d3a51182",
"metadata": {},
"outputs": [],
"source": [
"# %pip install -U langgraph langchain_community langchain_openai tavily-python"
]
"source": ["# %pip install -U langgraph langchain_community langchain_openai tavily-python"]
},
{
"cell_type": "code",
@@ -57,22 +55,7 @@
"id": "4215f9fb-71ff-4d88-8484-f73174db5592",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_if_undefined(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}=\")\n",
"\n",
"\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"ReWOO\"\n",
"_set_if_undefined(\"TAVILY_API_KEY\")\n",
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
"_set_if_undefined(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}=\")\n\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"ReWOO\"\n_set_if_undefined(\"TAVILY_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -90,17 +73,7 @@
"id": "9a92c875-c20b-4b7e-9d88-61c62382f8e2",
"metadata": {},
"outputs": [],
"source": [
"from typing import List, TypedDict\n",
"\n",
"\n",
"class ReWOO(TypedDict):\n",
" task: str\n",
" plan_string: str\n",
" steps: List\n",
" results: dict\n",
" result: str"
]
"source": ["from typing import List, TypedDict\n\n\nclass ReWOO(TypedDict):\n task: str\n plan_string: str\n steps: List\n results: dict\n result: str"]
},
{
"cell_type": "markdown",
@@ -127,11 +100,7 @@
"id": "c8836921-c89e-42b6-8c71-27aeaeac5368",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "code",
@@ -139,32 +108,7 @@
"id": "7e7faa92-30a1-4942-b3c7-acd3a7bfccbc",
"metadata": {},
"outputs": [],
"source": [
"prompt = \"\"\"For the following task, make plans that can solve the problem step by step. For each plan, indicate \\\n",
"which external tool together with tool input to retrieve evidence. You can store the evidence into a \\\n",
"variable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)\n",
"\n",
"Tools can be one of the following:\n",
"(1) Google[input]: Worker that searches results from Google. Useful when you need to find short\n",
"and succinct answers about a specific topic. The input should be a search query.\n",
"(2) LLM[input]: A pretrained LLM like yourself. Useful when you need to act with general\n",
"world knowledge and common sense. Prioritize it when you are confident in solving the problem\n",
"yourself. Input can be any instruction.\n",
"\n",
"For example,\n",
"Task: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x\n",
"hours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours\n",
"less than Toby. How many hours did Rebecca work?\n",
"Plan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve\n",
"with Wolfram Alpha. #E1 = WolframAlpha[Solve x + (2x 10) + ((2x 10) 8) = 157]\n",
"Plan: Find out the number of hours Thomas worked. #E2 = LLM[What is x, given #E1]\n",
"Plan: Calculate the number of hours Rebecca worked. #E3 = Calculator[(2 #E2 10) 8]\n",
"\n",
"Begin! \n",
"Describe your plans with rich details. Each Plan should be followed by only one #E.\n",
"\n",
"Task: {task}\"\"\""
]
"source": ["prompt = \"\"\"For the following task, make plans that can solve the problem step by step. For each plan, indicate \\\nwhich external tool together with tool input to retrieve evidence. You can store the evidence into a \\\nvariable #E that can be called by later tools. (Plan, #E1, Plan, #E2, Plan, ...)\n\nTools can be one of the following:\n(1) Google[input]: Worker that searches results from Google. Useful when you need to find short\nand succinct answers about a specific topic. The input should be a search query.\n(2) LLM[input]: A pretrained LLM like yourself. Useful when you need to act with general\nworld knowledge and common sense. Prioritize it when you are confident in solving the problem\nyourself. Input can be any instruction.\n\nFor example,\nTask: Thomas, Toby, and Rebecca worked a total of 157 hours in one week. Thomas worked x\nhours. Toby worked 10 hours less than twice what Thomas worked, and Rebecca worked 8 hours\nless than Toby. How many hours did Rebecca work?\nPlan: Given Thomas worked x hours, translate the problem into algebraic expressions and solve\nwith Wolfram Alpha. #E1 = WolframAlpha[Solve x + (2x 10) + ((2x 10) 8) = 157]\nPlan: Find out the number of hours Thomas worked. #E2 = LLM[What is x, given #E1]\nPlan: Calculate the number of hours Rebecca worked. #E3 = Calculator[(2 #E2 10) 8]\n\nBegin! \nDescribe your plans with rich details. Each Plan should be followed by only one #E.\n\nTask: {task}\"\"\""]
},
{
"cell_type": "code",
@@ -172,9 +116,7 @@
"id": "72b4ab0f-7215-4f4b-9407-0ebad8b13b92",
"metadata": {},
"outputs": [],
"source": [
"task = \"what is the hometown of the 2024 australian open winner\""
]
"source": ["task = \"what is the hometown of the 2024 australian open winner\""]
},
{
"cell_type": "code",
@@ -182,9 +124,7 @@
"id": "56ecb45b-ea76-4303-a4f3-51406fe8312a",
"metadata": {},
"outputs": [],
"source": [
"result = model.invoke(prompt.format(task=task))"
]
"source": ["result = model.invoke(prompt.format(task=task))"]
},
{
"cell_type": "code",
@@ -210,9 +150,7 @@
]
}
],
"source": [
"print(result.content)"
]
"source": ["print(result.content)"]
},
{
"cell_type": "markdown",
@@ -231,24 +169,7 @@
"id": "f9f042b6-90d8-430f-abf3-04ad2bb047c7",
"metadata": {},
"outputs": [],
"source": [
"import re\n",
"\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"\n",
"# Regex to match expressions of the form E#... = ...[...]\n",
"regex_pattern = r\"Plan:\\s*(.+)\\s*(#E\\d+)\\s*=\\s*(\\w+)\\s*\\[([^\\]]+)\\]\"\n",
"prompt_template = ChatPromptTemplate.from_messages([(\"user\", prompt)])\n",
"planner = prompt_template | model\n",
"\n",
"\n",
"def get_plan(state: ReWOO):\n",
" task = state[\"task\"]\n",
" result = planner.invoke({\"task\": task})\n",
" # Find all matches in the sample text\n",
" matches = re.findall(regex_pattern, result.content)\n",
" return {\"steps\": matches, \"plan_string\": result.content}"
]
"source": ["import re\n\nfrom langchain_core.prompts import ChatPromptTemplate\n\n# Regex to match expressions of the form E#... = ...[...]\nregex_pattern = r\"Plan:\\s*(.+)\\s*(#E\\d+)\\s*=\\s*(\\w+)\\s*\\[([^\\]]+)\\]\"\nprompt_template = ChatPromptTemplate.from_messages([(\"user\", prompt)])\nplanner = prompt_template | model\n\n\ndef get_plan(state: ReWOO):\n task = state[\"task\"]\n result = planner.invoke({\"task\": task})\n # Find all matches in the sample text\n matches = re.findall(regex_pattern, result.content)\n return {\"steps\": matches, \"plan_string\": result.content}"]
},
{
"cell_type": "markdown",
@@ -268,11 +189,7 @@
"id": "3412cfc4-6796-4295-aea4-7eeb304e10bd",
"metadata": {},
"outputs": [],
"source": [
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"\n",
"search = TavilySearchResults()"
]
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\nsearch = TavilySearchResults()"]
},
{
"cell_type": "code",
@@ -280,32 +197,7 @@
"id": "aa96fbac-28bc-4afe-ae35-ddb3383d1147",
"metadata": {},
"outputs": [],
"source": [
"def _get_current_task(state: ReWOO):\n",
" if state[\"results\"] is None:\n",
" return 1\n",
" if len(state[\"results\"]) == len(state[\"steps\"]):\n",
" return None\n",
" else:\n",
" return len(state[\"results\"]) + 1\n",
"\n",
"\n",
"def tool_execution(state: ReWOO):\n",
" \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n",
" _step = _get_current_task(state)\n",
" _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n",
" _results = state[\"results\"] or {}\n",
" for k, v in _results.items():\n",
" tool_input = tool_input.replace(k, v)\n",
" if tool == \"Google\":\n",
" result = search.invoke(tool_input)\n",
" elif tool == \"LLM\":\n",
" result = model.invoke(tool_input)\n",
" else:\n",
" raise ValueError\n",
" _results[step_name] = str(result)\n",
" return {\"results\": _results}"
]
"source": ["def _get_current_task(state: ReWOO):\n if state[\"results\"] is None:\n return 1\n if len(state[\"results\"]) == len(state[\"steps\"]):\n return None\n else:\n return len(state[\"results\"]) + 1\n\n\ndef tool_execution(state: ReWOO):\n \"\"\"Worker node that executes the tools of a given plan.\"\"\"\n _step = _get_current_task(state)\n _, step_name, tool, tool_input = state[\"steps\"][_step - 1]\n _results = state[\"results\"] or {}\n for k, v in _results.items():\n tool_input = tool_input.replace(k, v)\n if tool == \"Google\":\n result = search.invoke(tool_input)\n elif tool == \"LLM\":\n result = model.invoke(tool_input)\n else:\n raise ValueError\n _results[step_name] = str(result)\n return {\"results\": _results}"]
},
{
"cell_type": "markdown",
@@ -323,32 +215,7 @@
"id": "0a4d9851-8590-42be-8c53-9969ebff85f4",
"metadata": {},
"outputs": [],
"source": [
"solve_prompt = \"\"\"Solve the following task or problem. To solve the problem, we have made step-by-step Plan and \\\n",
"retrieved corresponding Evidence to each Plan. Use them with caution since long evidence might \\\n",
"contain irrelevant information.\n",
"\n",
"{plan}\n",
"\n",
"Now solve the question or task according to provided Evidence above. Respond with the answer\n",
"directly with no extra words.\n",
"\n",
"Task: {task}\n",
"Response:\"\"\"\n",
"\n",
"\n",
"def solve(state: ReWOO):\n",
" plan = \"\"\n",
" for _plan, step_name, tool, tool_input in state[\"steps\"]:\n",
" _results = state[\"results\"] or {}\n",
" for k, v in _results.items():\n",
" tool_input = tool_input.replace(k, v)\n",
" step_name = step_name.replace(k, v)\n",
" plan += f\"Plan: {_plan}\\n{step_name} = {tool}[{tool_input}]\"\n",
" prompt = solve_prompt.format(plan=plan, task=state[\"task\"])\n",
" result = model.invoke(prompt)\n",
" return {\"result\": result.content}"
]
"source": ["solve_prompt = \"\"\"Solve the following task or problem. To solve the problem, we have made step-by-step Plan and \\\nretrieved corresponding Evidence to each Plan. Use them with caution since long evidence might \\\ncontain irrelevant information.\n\n{plan}\n\nNow solve the question or task according to provided Evidence above. Respond with the answer\ndirectly with no extra words.\n\nTask: {task}\nResponse:\"\"\"\n\n\ndef solve(state: ReWOO):\n plan = \"\"\n for _plan, step_name, tool, tool_input in state[\"steps\"]:\n _results = state[\"results\"] or {}\n for k, v in _results.items():\n tool_input = tool_input.replace(k, v)\n step_name = step_name.replace(k, v)\n plan += f\"Plan: {_plan}\\n{step_name} = {tool}[{tool_input}]\"\n prompt = solve_prompt.format(plan=plan, task=state[\"task\"])\n result = model.invoke(prompt)\n return {\"result\": result.content}"]
},
{
"cell_type": "markdown",
@@ -366,16 +233,7 @@
"id": "73b235d7-fa83-4e84-9f2e-2908f16deb26",
"metadata": {},
"outputs": [],
"source": [
"def _route(state):\n",
" _step = _get_current_task(state)\n",
" if _step is None:\n",
" # We have executed all tasks\n",
" return \"solve\"\n",
" else:\n",
" # We are still executing tasks, loop back to the \"tool\" node\n",
" return \"tool\""
]
"source": ["def _route(state):\n _step = _get_current_task(state)\n if _step is None:\n # We have executed all tasks\n return \"solve\"\n else:\n # We are still executing tasks, loop back to the \"tool\" node\n return \"tool\""]
},
{
"cell_type": "code",
@@ -383,20 +241,7 @@
"id": "cf173aa1-ce31-4dca-8111-30c91e209652",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"graph = StateGraph(ReWOO)\n",
"graph.add_node(\"plan\", get_plan)\n",
"graph.add_node(\"tool\", tool_execution)\n",
"graph.add_node(\"solve\", solve)\n",
"graph.add_edge(\"plan\", \"tool\")\n",
"graph.add_edge(\"solve\", END)\n",
"graph.add_conditional_edges(\"tool\", _route)\n",
"graph.set_entry_point(\"plan\")\n",
"\n",
"app = graph.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\ngraph = StateGraph(ReWOO)\ngraph.add_node(\"plan\", get_plan)\ngraph.add_node(\"tool\", tool_execution)\ngraph.add_node(\"solve\", solve)\ngraph.add_edge(\"plan\", \"tool\")\ngraph.add_edge(\"solve\", END)\ngraph.add_conditional_edges(\"tool\", _route)\ngraph.add_edge(START, \"plan\")\n\napp = graph.compile()"]
},
{
"cell_type": "code",
@@ -425,11 +270,7 @@
]
}
],
"source": [
"for s in app.stream({\"task\": task}):\n",
" print(s)\n",
" print(\"---\")"
]
"source": ["for s in app.stream({\"task\": task}):\n print(s)\n print(\"---\")"]
},
{
"cell_type": "code",
@@ -445,10 +286,7 @@
]
}
],
"source": [
"# Print out the final result\n",
"print(s[END][\"result\"])"
]
"source": ["# Print out the final result\nprint(s[END][\"result\"])"]
},
{
"cell_type": "markdown",
+2 -2
View File
@@ -372,7 +372,7 @@
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"from langgraph.graph import END, StateGraph, START\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
@@ -383,7 +383,7 @@
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"workflow.add_edge(START, \"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
+13 -162
View File
@@ -32,10 +32,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
},
{
"cell_type": "markdown",
@@ -51,18 +48,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -78,10 +64,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -101,20 +84,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\"The answer to your question lies within.\"]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\"The answer to your question lies within.\"]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -133,11 +103,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"tool_executor = ToolExecutor(tools)"
]
"source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
},
{
"cell_type": "markdown",
@@ -161,11 +127,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "markdown",
@@ -183,9 +145,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -211,17 +171,7 @@
"id": "ea793afa-2eab-4901-910d-6eed90cd6564",
"metadata": {},
"outputs": [],
"source": [
"import operator\n",
"from typing import Annotated, Sequence\n",
"\n",
"from langchain_core.messages import BaseMessage\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"\n",
"\n",
"class AgentState(BaseModel):\n",
" messages: Annotated[Sequence[BaseMessage], operator.add]"
]
"source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom langchain_core.messages import BaseMessage\nfrom langchain_core.pydantic_v1 import BaseModel\n\n\nclass AgentState(BaseModel):\n messages: Annotated[Sequence[BaseMessage], operator.add]"]
},
{
"cell_type": "markdown",
@@ -260,53 +210,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state.messages\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state.messages\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state.messages\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}"
]
"source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state.messages\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state.messages\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state):\n messages = state.messages\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation from the function_call\n tool_call = last_message.tool_calls[0]\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n # We call the tool_executor and get back a response\n response = tool_executor.invoke(action)\n # We use the response to create a ToolMessage\n tool_message = ToolMessage(\n content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n )\n # We return a list, because this will get added to the existing list\n return {\"messages\": [tool_message]}"]
},
{
"cell_type": "markdown",
@@ -324,50 +228,7 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(AgentState)\n",
"\n",
"# Define the two nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")\n",
"\n",
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile()"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
},
{
"cell_type": "code",
@@ -386,11 +247,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"display(Image(app.get_graph().draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -432,13 +289,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
"for chunk in app.stream(inputs, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor chunk in app.stream(inputs, stream_mode=\"values\"):\n chunk[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "code",
@@ -446,7 +297,7 @@
"id": "296c7456-da05-4326-95dc-47d6b312da9d",
"metadata": {},
"outputs": [],
"source": []
"source": [""]
}
],
"metadata": {
File diff suppressed because it is too large Load Diff
+12 -126
View File
@@ -22,10 +22,7 @@
"id": "c04a3f8e-0bc9-430b-85db-3edfa026d2cd",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-openai"]
},
{
"cell_type": "code",
@@ -41,18 +38,7 @@
]
}
],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -68,35 +54,7 @@
"id": "1d51c35c-dbf2-4c01-932d-c5d308ea37d2",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.runnables import ConfigurableField\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"final_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
"\n",
"model = model.bind_tools(tools)\n",
"# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\n",
"final_model = final_model.with_config(tags=[\"final_node\"])"
]
"source": ["from typing import Literal\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.runnables import ConfigurableField\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef get_weather(city: Literal[\"nyc\", \"sf\"]):\n \"\"\"Use this to get weather information.\"\"\"\n if city == \"nyc\":\n return \"It might be cloudy in nyc\"\n elif city == \"sf\":\n return \"It's always sunny in sf\"\n else:\n raise AssertionError(\"Unknown city\")\n\n\ntools = [get_weather]\nmodel = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\nfinal_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\nmodel = model.bind_tools(tools)\n# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\nfinal_model = final_model.with_config(tags=[\"final_node\"])"]
},
{
"cell_type": "code",
@@ -104,9 +62,7 @@
"id": "0af37212-e592-484d-9194-35d53fa79678",
"metadata": {},
"outputs": [],
"source": [
"tool_node = ToolNode(tools=tools)"
]
"source": ["tool_node = ToolNode(tools=tools)"]
},
{
"cell_type": "code",
@@ -114,13 +70,7 @@
"id": "ac9d4f5b-655a-48f3-b514-a4a0815714a6",
"metadata": {},
"outputs": [],
"source": [
"from typing import TypedDict, Annotated\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"from langgraph.graph.message import MessagesState\n",
"from langchain_core.messages import BaseMessage"
]
"source": ["from typing import TypedDict, Annotated\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import MessagesState\nfrom langchain_core.messages import BaseMessage"]
},
{
"cell_type": "markdown",
@@ -136,9 +86,7 @@
"id": "3948c6b8-0317-4001-b699-32b25306a023",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import SystemMessage, HumanMessage"
]
"source": ["from langchain_core.messages import SystemMessage, HumanMessage"]
},
{
"cell_type": "code",
@@ -146,35 +94,7 @@
"id": "2efe9fb4-c6c2-4171-becd-d45bbf899209",
"metadata": {},
"outputs": [],
"source": [
"def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n",
" messages = state['messages']\n",
" last_message = messages[-1]\n",
" # If the LLM makes a tool call, then we route to the \"tools\" node\n",
" if last_message.tool_calls:\n",
" return \"tools\"\n",
" # Otherwise, we stop (reply to the user)\n",
" return \"final\"\n",
"\n",
"\n",
"def call_model(state: MessagesState):\n",
" messages = state['messages']\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"def call_final_model(state: MessagesState):\n",
" messages = state['messages']\n",
" last_ai_message = messages[-1]\n",
" response = final_model.invoke([\n",
" SystemMessage(\"Rewrite this in the voice of Al Roker\"),\n",
" HumanMessage(last_ai_message.content)\n",
" ])\n",
" # overwrite the last AI message from the agent\n",
" response.id = last_ai_message.id\n",
" return {\"messages\": [response]}"
]
"source": ["def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n messages = state['messages']\n last_message = messages[-1]\n # If the LLM makes a tool call, then we route to the \"tools\" node\n if last_message.tool_calls:\n return \"tools\"\n # Otherwise, we stop (reply to the user)\n return \"final\"\n\n\ndef call_model(state: MessagesState):\n messages = state['messages']\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef call_final_model(state: MessagesState):\n messages = state['messages']\n last_ai_message = messages[-1]\n response = final_model.invoke([\n SystemMessage(\"Rewrite this in the voice of Al Roker\"),\n HumanMessage(last_ai_message.content)\n ])\n # overwrite the last AI message from the agent\n response.id = last_ai_message.id\n return {\"messages\": [response]}"]
},
{
"cell_type": "code",
@@ -182,23 +102,7 @@
"id": "b1a9a981-8629-4d25-a0e1-d666c3968b30",
"metadata": {},
"outputs": [],
"source": [
"workflow = StateGraph(MessagesState)\n",
"\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"tools\", tool_node)\n",
"# add a separate final node\n",
"workflow.add_node(\"final\", call_final_model)\n",
"\n",
"workflow.set_entry_point(\"agent\")\n",
"workflow.add_conditional_edges(\n",
" \"agent\",\n",
" should_continue,\n",
")\n",
"\n",
"workflow.add_edge(\"tools\", 'agent')\n",
"workflow.add_edge(\"final\", END)"
]
"source": ["workflow = StateGraph(MessagesState)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"tools\", tool_node)\n# add a separate final node\nworkflow.add_node(\"final\", call_final_model)\n\nworkflow.add_edge(START, \"agent\")\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n)\n\nworkflow.add_edge(\"tools\", 'agent')\nworkflow.add_edge(\"final\", END)"]
},
{
"cell_type": "code",
@@ -206,9 +110,7 @@
"id": "a7b0251f-dcee-49d6-8133-af50d4a55e22",
"metadata": {},
"outputs": [],
"source": [
"app = workflow.compile()"
]
"source": ["app = workflow.compile()"]
},
{
"cell_type": "code",
@@ -216,9 +118,7 @@
"id": "f8b77e74-17e9-4fee-a164-4637013b55ff",
"metadata": {},
"outputs": [],
"source": [
"from IPython.display import display, Image"
]
"source": ["from IPython.display import display, Image"]
},
{
"cell_type": "code",
@@ -237,9 +137,7 @@
"output_type": "display_data"
}
],
"source": [
"display(Image(app.get_graph().draw_mermaid_png()))"
]
"source": ["display(Image(app.get_graph().draw_mermaid_png()))"]
},
{
"cell_type": "markdown",
@@ -271,19 +169,7 @@
]
}
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\n",
"async for event in app.astream_events(inputs, version=\"v2\"):\n",
" kind = event[\"event\"]\n",
" tags = event.get(\"tags\", [])\n",
" if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n",
" data = event[\"data\"]\n",
" if data[\"chunk\"].content:\n",
" # Empty content in the context of OpenAI or Anthropic usually means\n",
" # that the model is asking for a tool to be invoked.\n",
" # So we only print non-empty content\n",
" print(data[\"chunk\"].content, end=\"|\")"
]
"source": ["inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\nasync for event in app.astream_events(inputs, version=\"v2\"):\n kind = event[\"event\"]\n tags = event.get(\"tags\", [])\n if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n data = event[\"data\"]\n if data[\"chunk\"].content:\n # Empty content in the context of OpenAI or Anthropic usually means\n # that the model is asking for a tool to be invoked.\n # So we only print non-empty content\n print(data[\"chunk\"].content, end=\"|\")"]
}
],
"metadata": {
+9 -156
View File
@@ -22,10 +22,7 @@
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph"]
},
{
"cell_type": "markdown",
@@ -39,19 +36,7 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -69,60 +54,7 @@
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph\n",
"\n",
"\n",
"def reduce_list(left: list | None, right: list | None) -> list:\n",
" if not left:\n",
" left = []\n",
" if not right:\n",
" right = []\n",
" return left + right\n",
"\n",
"\n",
"class ChildState(TypedDict):\n",
" name: str\n",
" path: Annotated[list[str], reduce_list]\n",
"\n",
"\n",
"class ParentState(TypedDict):\n",
" name: str\n",
" path: Annotated[list[str], reduce_list]\n",
"\n",
"\n",
"child_builder = StateGraph(ChildState)\n",
"\n",
"child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
"child_builder.set_entry_point(\"child_start\")\n",
"child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
"child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n",
"child_builder.add_edge(\"child_start\", \"child_middle\")\n",
"child_builder.add_edge(\"child_middle\", \"child_end\")\n",
"child_builder.set_finish_point(\"child_end\")\n",
"\n",
"builder = StateGraph(ParentState)\n",
"\n",
"builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
"builder.set_entry_point(\"grandparent\")\n",
"builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
"builder.add_node(\"child\", child_builder.compile())\n",
"builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
"builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
"\n",
"# Add connections\n",
"builder.add_edge(\"grandparent\", \"parent\")\n",
"builder.add_edge(\"parent\", \"child\")\n",
"builder.add_edge(\"parent\", \"sibling\")\n",
"builder.add_edge(\"child\", \"fin\")\n",
"builder.add_edge(\"sibling\", \"fin\")\n",
"builder.set_finish_point(\"fin\")\n",
"graph = builder.compile()"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n if not left:\n left = []\n if not right:\n right = []\n return left + right\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nchild_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.set_finish_point(\"child_end\")\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.set_finish_point(\"fin\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -140,12 +72,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"# Setting xray to 1 will show the internal structure of the nested graph\n",
"display(Image(graph.get_graph(xray=1).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -235,9 +162,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"name\": \"test\"}, debug=True)"
]
"source": ["graph.invoke({\"name\": \"test\"}, debug=True)"]
},
{
"cell_type": "markdown",
@@ -257,79 +182,14 @@
"execution_count": 23,
"metadata": {},
"outputs": [],
"source": [
"import uuid\n",
"\n",
"\n",
"def reduce_list(left: list | None, right: list | None) -> list:\n",
" \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n",
" if not left:\n",
" left = []\n",
" if not right:\n",
" right = []\n",
" left_, right_ = [], []\n",
" for orig, new in [(left, left_), (right, right_)]:\n",
" for val in orig:\n",
" if not isinstance(val, dict):\n",
" val = {\"val\": val}\n",
" if \"id\" not in val:\n",
" val[\"id\"] = str(uuid.uuid4())\n",
" new.append(val)\n",
" # Merge the two lists\n",
" left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n",
" merged = left_.copy()\n",
" for val in right_:\n",
" if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n",
" merged[existing_idx] = val\n",
" else:\n",
" merged.append(val)\n",
" return merged\n",
"\n",
"\n",
"class ChildState(TypedDict):\n",
" name: str\n",
" path: Annotated[list[str], reduce_list]\n",
"\n",
"\n",
"class ParentState(TypedDict):\n",
" name: str\n",
" path: Annotated[list[str], reduce_list]"
]
"source": ["import uuid\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n if not left:\n left = []\n if not right:\n right = []\n left_, right_ = [], []\n for orig, new in [(left, left_), (right, right_)]:\n for val in orig:\n if not isinstance(val, dict):\n val = {\"val\": val}\n if \"id\" not in val:\n val[\"id\"] = str(uuid.uuid4())\n new.append(val)\n # Merge the two lists\n left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n merged = left_.copy()\n for val in right_:\n if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n merged[existing_idx] = val\n else:\n merged.append(val)\n return merged\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]"]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"child_builder = StateGraph(ChildState)\n",
"\n",
"child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
"child_builder.set_entry_point(\"child_start\")\n",
"child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
"child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n",
"child_builder.add_edge(\"child_start\", \"child_middle\")\n",
"child_builder.add_edge(\"child_middle\", \"child_end\")\n",
"child_builder.set_finish_point(\"child_end\")\n",
"\n",
"builder = StateGraph(ParentState)\n",
"\n",
"builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
"builder.set_entry_point(\"grandparent\")\n",
"builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
"builder.add_node(\"child\", child_builder.compile())\n",
"builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
"builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
"\n",
"# Add connections\n",
"builder.add_edge(\"grandparent\", \"parent\")\n",
"builder.add_edge(\"parent\", \"child\")\n",
"builder.add_edge(\"parent\", \"sibling\")\n",
"builder.add_edge(\"child\", \"fin\")\n",
"builder.add_edge(\"sibling\", \"fin\")\n",
"builder.set_finish_point(\"fin\")\n",
"graph = builder.compile()"
]
"source": ["child_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.set_finish_point(\"child_end\")\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.set_finish_point(\"fin\")\ngraph = builder.compile()"]
},
{
"cell_type": "code",
@@ -347,12 +207,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"# Setting xray to 1 will show the internal structure of the nested graph\n",
"display(Image(graph.get_graph(xray=1).draw_mermaid_png()))"
]
"source": ["from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))"]
},
{
"cell_type": "code",
@@ -446,9 +301,7 @@
"output_type": "execute_result"
}
],
"source": [
"graph.invoke({\"name\": \"test\"}, debug=True)"
]
"source": ["graph.invoke({\"name\": \"test\"}, debug=True)"]
}
],
"metadata": {
+34 -236
View File
@@ -46,10 +46,7 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
},
{
"cell_type": "markdown",
@@ -65,18 +62,7 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"ANTHROPIC_API_KEY\")"
]
"source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -92,10 +78,7 @@
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
},
{
"cell_type": "markdown",
@@ -113,22 +96,7 @@
"id": "f5319e01",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"# `add_messages`` essentially does this\n",
"# (with more robust handling)\n",
"# def add_messages(left: list, right: list):\n",
"# return left + right\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]"
]
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
},
{
"cell_type": "markdown",
@@ -148,19 +116,7 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" return [\"The weather is cloudy with a chance of meatballs.\"]\n",
"\n",
"\n",
"tools = [search]"
]
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The weather is cloudy with a chance of meatballs.\"]\n\n\ntools = [search]"]
},
{
"cell_type": "markdown",
@@ -177,11 +133,7 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.prebuilt import ToolNode\n",
"\n",
"tool_node = ToolNode(tools)"
]
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
},
{
"cell_type": "markdown",
@@ -205,11 +157,7 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(temperature=0)"
]
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
},
{
"cell_type": "markdown",
@@ -227,9 +175,7 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
"source": [
"model = model.bind_tools(tools)"
]
"source": ["model = model.bind_tools(tools)"]
},
{
"cell_type": "markdown",
@@ -264,20 +210,7 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n",
" last_message = state[\"messages\"][-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\""
]
"source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""]
},
{
"cell_type": "markdown",
@@ -295,50 +228,7 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"\n",
"# Define the two nodes we will cycle between\n",
"def call_model(state: State) -> State:\n",
" return {\"messages\": model.invoke(state[\"messages\"])}\n",
"\n",
"\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", tool_node)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# We now add a normal edge from `tools` to `agent`.\n",
"# This means that after `tools` is called, `agent` node is called next.\n",
"workflow.add_edge(\"action\", \"agent\")"
]
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Define the two nodes we will cycle between\ndef call_model(state: State) -> State:\n return {\"messages\": model.invoke(state[\"messages\"])}\n\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
},
{
"cell_type": "markdown",
@@ -356,11 +246,7 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
"\n",
"memory = SqliteSaver.from_conn_string(\":memory:\")"
]
"source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
},
{
"cell_type": "code",
@@ -368,12 +254,7 @@
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
"metadata": {},
"outputs": [],
"source": [
"# Finally, we compile it!\n",
"# This compiles it into a LangChain Runnable,\n",
"# meaning you can use it as you would any other runnable\n",
"app = workflow.compile(checkpointer=memory)"
]
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"]
},
{
"cell_type": "markdown",
@@ -400,15 +281,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"\n",
"try:\n",
" display(Image(app.get_graph().draw_mermaid_png()))\n",
"except Exception:\n",
" # This requires some extra dependencies and is optional\n",
" pass"
]
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
},
{
"cell_type": "markdown",
@@ -439,14 +312,7 @@
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"hi! I'm bob\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -484,9 +350,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.get_state(config).values"
]
"source": ["app.get_state(config).values"]
},
{
"cell_type": "markdown",
@@ -515,9 +379,7 @@
"output_type": "execute_result"
}
],
"source": [
"app.get_state(config).next"
]
"source": ["app.get_state(config).next"]
},
{
"cell_type": "markdown",
@@ -564,12 +426,7 @@
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"what is the weather in sf currently\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["config = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -609,9 +466,7 @@
"id": "5a68afc0-606f-4294-a872-b2b563be0d69",
"metadata": {},
"outputs": [],
"source": [
"app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
"source": ["app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
},
{
"cell_type": "code",
@@ -635,14 +490,7 @@
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"4\"}}\n",
"input_message = HumanMessage(content=\"what is the weather in sf currently\")\n",
"for event in app_w_interrupt.stream(\n",
" {\"messages\": [input_message]}, config, stream_mode=\"values\"\n",
"):\n",
" event[\"messages\"][-1].pretty_print()"
]
"source": ["config = {\"configurable\": {\"thread_id\": \"4\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app_w_interrupt.stream(\n {\"messages\": [input_message]}, config, stream_mode=\"values\"\n):\n event[\"messages\"][-1].pretty_print()"]
},
{
"cell_type": "markdown",
@@ -678,10 +526,7 @@
"output_type": "execute_result"
}
],
"source": [
"current_values = app_w_interrupt.get_state(config)\n",
"current_values.next"
]
"source": ["current_values = app_w_interrupt.get_state(config)\ncurrent_values.next"]
},
{
"cell_type": "markdown",
@@ -710,9 +555,7 @@
"output_type": "execute_result"
}
],
"source": [
"current_values.values[\"messages\"][-1].tool_calls"
]
"source": ["current_values.values[\"messages\"][-1].tool_calls"]
},
{
"cell_type": "markdown",
@@ -728,11 +571,7 @@
"id": "060e2e33-1f6a-40ef-850e-161b308986fb",
"metadata": {},
"outputs": [],
"source": [
"current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n",
" \"query\"\n",
"] = \"weather in San Francisco today\""
]
"source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco today\""]
},
{
"cell_type": "code",
@@ -752,9 +591,7 @@
"output_type": "execute_result"
}
],
"source": [
"app_w_interrupt.update_state(config, current_values.values)"
]
"source": ["app_w_interrupt.update_state(config, current_values.values)"]
},
{
"cell_type": "markdown",
@@ -792,9 +629,7 @@
"output_type": "execute_result"
}
],
"source": [
"app_w_interrupt.get_state(config).values"
]
"source": ["app_w_interrupt.get_state(config).values"]
},
{
"cell_type": "code",
@@ -813,9 +648,7 @@
"output_type": "execute_result"
}
],
"source": [
"app_w_interrupt.get_state(config).next"
]
"source": ["app_w_interrupt.get_state(config).next"]
},
{
"cell_type": "markdown",
@@ -846,11 +679,7 @@
]
}
],
"source": [
"for event in app_w_interrupt.stream(None, config):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["for event in app_w_interrupt.stream(None, config):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "markdown",
@@ -897,13 +726,7 @@
]
}
],
"source": [
"for state in app_w_interrupt.get_state_history(config):\n",
" print(state)\n",
" print(\"--\")\n",
" if len(state.values[\"messages\"]) == 2:\n",
" to_replay = state"
]
"source": ["for state in app_w_interrupt.get_state_history(config):\n print(state)\n print(\"--\")\n if len(state.values[\"messages\"]) == 2:\n to_replay = state"]
},
{
"cell_type": "markdown",
@@ -931,9 +754,7 @@
"output_type": "execute_result"
}
],
"source": [
"to_replay.values"
]
"source": ["to_replay.values"]
},
{
"cell_type": "code",
@@ -952,9 +773,7 @@
"output_type": "execute_result"
}
],
"source": [
"to_replay.next"
]
"source": ["to_replay.next"]
},
{
"cell_type": "markdown",
@@ -987,11 +806,7 @@
]
}
],
"source": [
"for event in app_w_interrupt.stream(None, to_replay.config):\n",
" for v in event.values():\n",
" print(v)"
]
"source": ["for event in app_w_interrupt.stream(None, to_replay.config):\n for v in event.values():\n print(v)"]
},
{
"cell_type": "markdown",
@@ -1019,18 +834,7 @@
"id": "b084f141-5800-487b-b115-d2e58421b963",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"\n",
"branch_config = app_w_interrupt.update_state(\n",
" to_replay.config,\n",
" {\n",
" \"messages\": [\n",
" AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n",
" ]\n",
" },\n",
")"
]
"source": ["from langchain_core.messages import AIMessage\n\nbranch_config = app_w_interrupt.update_state(\n to_replay.config,\n {\n \"messages\": [\n AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n ]\n },\n)"]
},
{
"cell_type": "code",
@@ -1038,9 +842,7 @@
"id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641",
"metadata": {},
"outputs": [],
"source": [
"branch_state = app_w_interrupt.get_state(branch_config)"
]
"source": ["branch_state = app_w_interrupt.get_state(branch_config)"]
},
{
"cell_type": "code",
@@ -1060,9 +862,7 @@
"output_type": "execute_result"
}
],
"source": [
"branch_state.values"
]
"source": ["branch_state.values"]
},
{
"cell_type": "code",
@@ -1081,9 +881,7 @@
"output_type": "execute_result"
}
],
"source": [
"branch_state.next"
]
"source": ["branch_state.next"]
},
{
"cell_type": "markdown",
File diff suppressed because one or more lines are too long
+24 -587
View File
@@ -38,12 +38,7 @@
"id": "abd95235-4da5-4d6a-985f-78b2572ad626",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain_anthropic langsmith\n",
"# For the embedding-based classifier use in phase 2\n",
"%pip install -U sklearn langchain_openai"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph langchain_anthropic langsmith\n# For the embedding-based classifier use in phase 2\n%pip install -U sklearn langchain_openai"]
},
{
"cell_type": "code",
@@ -51,20 +46,7 @@
"id": "d98b62e4-d327-4442-8482-65529500a8a7",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from getpass import getpass\n",
"\n",
"if \"ANTHROPIC_API_KEY\" not in os.environ:\n",
" os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"Enter your ANTHROPIC_API_KEY: \")\n",
"\n",
"# (Optional) Enable tracing\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"tnt-llm\"\n",
"\n",
"if \"LANGCHAIN_API_KEY\" not in os.environ:\n",
" os.environ[\"LANGCHAIN_API_KEY\"] = getpass(\"Enter your LANGCHAIN_API_KEY: \")"
]
"source": ["import os\nfrom getpass import getpass\n\nif \"ANTHROPIC_API_KEY\" not in os.environ:\n os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"Enter your ANTHROPIC_API_KEY: \")\n\n# (Optional) Enable tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"tnt-llm\"\n\nif \"LANGCHAIN_API_KEY\" not in os.environ:\n os.environ[\"LANGCHAIN_API_KEY\"] = getpass(\"Enter your LANGCHAIN_API_KEY: \")"]
},
{
"cell_type": "markdown",
@@ -84,31 +66,7 @@
"id": "580d82b5-b60c-47a4-9c8b-e28be22ca0e3",
"metadata": {},
"outputs": [],
"source": [
"import logging\n",
"import operator\n",
"from typing import Annotated, List, Optional, TypedDict\n",
"\n",
"logging.basicConfig(level=logging.WARNING)\n",
"logger = logging.getLogger(\"tnt-llm\")\n",
"\n",
"\n",
"class Doc(TypedDict):\n",
" id: str\n",
" content: str\n",
" summary: Optional[str]\n",
" explanation: Optional[str]\n",
" category: Optional[str]\n",
"\n",
"\n",
"class TaxonomyGenerationState(TypedDict):\n",
" # The raw docs; we inject summaries within them in the first step\n",
" documents: List[Doc]\n",
" # Indices to be concise\n",
" minibatches: List[List[int]]\n",
" # Candidate Taxonomies (full trajectory)\n",
" clusters: Annotated[List[List[dict]], operator.add]"
]
"source": ["import logging\nimport operator\nfrom typing import Annotated, List, Optional, TypedDict\n\nlogging.basicConfig(level=logging.WARNING)\nlogger = logging.getLogger(\"tnt-llm\")\n\n\nclass Doc(TypedDict):\n id: str\n content: str\n summary: Optional[str]\n explanation: Optional[str]\n category: Optional[str]\n\n\nclass TaxonomyGenerationState(TypedDict):\n # The raw docs; we inject summaries within them in the first step\n documents: List[Doc]\n # Indices to be concise\n minibatches: List[List[int]]\n # Candidate Taxonomies (full trajectory)\n clusters: Annotated[List[List[dict]], operator.add]"]
},
{
"cell_type": "markdown",
@@ -126,77 +84,7 @@
"id": "ff02c2a1-18b5-4848-96bb-27ff00978570",
"metadata": {},
"outputs": [],
"source": [
"import re\n",
"\n",
"from langchain import hub\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.output_parsers import StrOutputParser\n",
"from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough\n",
"\n",
"summary_prompt = hub.pull(\"wfh/tnt-llm-summary-generation\").partial(\n",
" summary_length=20, explanation_length=30\n",
")\n",
"\n",
"\n",
"def parse_summary(xml_string: str) -> dict:\n",
" summary_pattern = r\"<summary>(.*?)</summary>\"\n",
" explanation_pattern = r\"<explanation>(.*?)</explanation>\"\n",
"\n",
" summary_match = re.search(summary_pattern, xml_string, re.DOTALL)\n",
" explanation_match = re.search(explanation_pattern, xml_string, re.DOTALL)\n",
"\n",
" summary = summary_match.group(1).strip() if summary_match else \"\"\n",
" explanation = explanation_match.group(1).strip() if explanation_match else \"\"\n",
"\n",
" return {\"summary\": summary, \"explanation\": explanation}\n",
"\n",
"\n",
"summary_llm_chain = (\n",
" summary_prompt\n",
" | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
" | StrOutputParser()\n",
" # Customize the tracing name for easier organization\n",
").with_config(run_name=\"GenerateSummary\")\n",
"summary_chain = summary_llm_chain | parse_summary\n",
"\n",
"\n",
"# Now combine as a \"map\" operation in a map-reduce chain\n",
"# Input: state\n",
"# Output: state U summaries\n",
"# Processes docs in parallel\n",
"def get_content(state: TaxonomyGenerationState):\n",
" docs = state[\"documents\"]\n",
" return [{\"content\": doc[\"content\"]} for doc in docs]\n",
"\n",
"\n",
"map_step = RunnablePassthrough.assign(\n",
" summaries=get_content\n",
" # This effectively creates a \"map\" operation\n",
" # Note you can make this more robust by handling individual errors\n",
" | RunnableLambda(func=summary_chain.batch, afunc=summary_chain.abatch)\n",
")\n",
"\n",
"\n",
"def reduce_summaries(combined: dict) -> TaxonomyGenerationState:\n",
" summaries = combined[\"summaries\"]\n",
" documents = combined[\"documents\"]\n",
" return {\n",
" \"documents\": [\n",
" {\n",
" \"id\": doc[\"id\"],\n",
" \"content\": doc[\"content\"],\n",
" \"summary\": summ_info[\"summary\"],\n",
" \"explanation\": summ_info[\"explanation\"],\n",
" }\n",
" for doc, summ_info in zip(documents, summaries)\n",
" ]\n",
" }\n",
"\n",
"\n",
"# This is actually the node itself!\n",
"map_reduce_chain = map_step | reduce_summaries"
]
"source": ["import re\n\nfrom langchain import hub\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough\n\nsummary_prompt = hub.pull(\"wfh/tnt-llm-summary-generation\").partial(\n summary_length=20, explanation_length=30\n)\n\n\ndef parse_summary(xml_string: str) -> dict:\n summary_pattern = r\"<summary>(.*?)</summary>\"\n explanation_pattern = r\"<explanation>(.*?)</explanation>\"\n\n summary_match = re.search(summary_pattern, xml_string, re.DOTALL)\n explanation_match = re.search(explanation_pattern, xml_string, re.DOTALL)\n\n summary = summary_match.group(1).strip() if summary_match else \"\"\n explanation = explanation_match.group(1).strip() if explanation_match else \"\"\n\n return {\"summary\": summary, \"explanation\": explanation}\n\n\nsummary_llm_chain = (\n summary_prompt\n | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n | StrOutputParser()\n # Customize the tracing name for easier organization\n).with_config(run_name=\"GenerateSummary\")\nsummary_chain = summary_llm_chain | parse_summary\n\n\n# Now combine as a \"map\" operation in a map-reduce chain\n# Input: state\n# Output: state U summaries\n# Processes docs in parallel\ndef get_content(state: TaxonomyGenerationState):\n docs = state[\"documents\"]\n return [{\"content\": doc[\"content\"]} for doc in docs]\n\n\nmap_step = RunnablePassthrough.assign(\n summaries=get_content\n # This effectively creates a \"map\" operation\n # Note you can make this more robust by handling individual errors\n | RunnableLambda(func=summary_chain.batch, afunc=summary_chain.abatch)\n)\n\n\ndef reduce_summaries(combined: dict) -> TaxonomyGenerationState:\n summaries = combined[\"summaries\"]\n documents = combined[\"documents\"]\n return {\n \"documents\": [\n {\n \"id\": doc[\"id\"],\n \"content\": doc[\"content\"],\n \"summary\": summ_info[\"summary\"],\n \"explanation\": summ_info[\"explanation\"],\n }\n for doc, summ_info in zip(documents, summaries)\n ]\n }\n\n\n# This is actually the node itself!\nmap_reduce_chain = map_step | reduce_summaries"]
},
{
"cell_type": "markdown",
@@ -214,36 +102,7 @@
"id": "3e0139c3-b5ba-42b9-9367-33533d66eb58",
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"\n",
"\n",
"def get_minibatches(state: TaxonomyGenerationState, config: RunnableConfig):\n",
" batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n",
" original = state[\"documents\"]\n",
" indices = list(range(len(original)))\n",
" random.shuffle(indices)\n",
" if len(indices) < batch_size:\n",
" # Don't pad needlessly if we can't fill a single batch\n",
" return [indices]\n",
"\n",
" num_full_batches = len(indices) // batch_size\n",
"\n",
" batches = [\n",
" indices[i * batch_size : (i + 1) * batch_size] for i in range(num_full_batches)\n",
" ]\n",
"\n",
" leftovers = len(indices) % batch_size\n",
" if leftovers:\n",
" last_batch = indices[num_full_batches * batch_size :]\n",
" elements_to_add = batch_size - leftovers\n",
" last_batch += random.sample(indices, elements_to_add)\n",
" batches.append(last_batch)\n",
"\n",
" return {\n",
" \"minibatches\": batches,\n",
" }"
]
"source": ["import random\n\n\ndef get_minibatches(state: TaxonomyGenerationState, config: RunnableConfig):\n batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n original = state[\"documents\"]\n indices = list(range(len(original)))\n random.shuffle(indices)\n if len(indices) < batch_size:\n # Don't pad needlessly if we can't fill a single batch\n return [indices]\n\n num_full_batches = len(indices) // batch_size\n\n batches = [\n indices[i * batch_size : (i + 1) * batch_size] for i in range(num_full_batches)\n ]\n\n leftovers = len(indices) % batch_size\n if leftovers:\n last_batch = indices[num_full_batches * batch_size :]\n elements_to_add = batch_size - leftovers\n last_batch += random.sample(indices, elements_to_add)\n batches.append(last_batch)\n\n return {\n \"minibatches\": batches,\n }"]
},
{
"cell_type": "markdown",
@@ -261,80 +120,7 @@
"id": "224ed013-2963-489c-b734-315cad701d59",
"metadata": {},
"outputs": [],
"source": [
"from typing import Dict\n",
"\n",
"from langchain_core.runnables import Runnable\n",
"\n",
"\n",
"def parse_taxa(output_text: str) -> Dict:\n",
" \"\"\"Extract the taxonomy from the generated output.\"\"\"\n",
" cluster_matches = re.findall(\n",
" r\"\\s*<id>(.*?)</id>\\s*<name>(.*?)</name>\\s*<description>(.*?)</description>\\s*\",\n",
" output_text,\n",
" re.DOTALL,\n",
" )\n",
" clusters = [\n",
" {\"id\": id.strip(), \"name\": name.strip(), \"description\": description.strip()}\n",
" for id, name, description in cluster_matches\n",
" ]\n",
" # We don't parse the explanation since it isn't used downstream\n",
" return {\"clusters\": clusters}\n",
"\n",
"\n",
"def format_docs(docs: List[Doc]) -> str:\n",
" xml_table = \"<conversations>\\n\"\n",
" for doc in docs:\n",
" xml_table += f'<conv_summ id={doc[\"id\"]}>{doc[\"summary\"]}</conv_summ>\\n'\n",
" xml_table += \"</conversations>\"\n",
" return xml_table\n",
"\n",
"\n",
"def format_taxonomy(clusters):\n",
" xml = \"<cluster_table>\\n\"\n",
" for label in clusters:\n",
" xml += \" <cluster>\\n\"\n",
" xml += f' <id>{label[\"id\"]}</id>\\n'\n",
" xml += f' <name>{label[\"name\"]}</name>\\n'\n",
" xml += f' <description>{label[\"description\"]}</description>\\n'\n",
" xml += \" </cluster>\\n\"\n",
" xml += \"</cluster_table>\"\n",
" return xml\n",
"\n",
"\n",
"def invoke_taxonomy_chain(\n",
" chain: Runnable,\n",
" state: TaxonomyGenerationState,\n",
" config: RunnableConfig,\n",
" mb_indices: List[int],\n",
") -> TaxonomyGenerationState:\n",
" configurable = config[\"configurable\"]\n",
" docs = state[\"documents\"]\n",
" minibatch = [docs[idx] for idx in mb_indices]\n",
" data_table_xml = format_docs(minibatch)\n",
"\n",
" previous_taxonomy = state[\"clusters\"][-1] if state[\"clusters\"] else []\n",
" cluster_table_xml = format_taxonomy(previous_taxonomy)\n",
"\n",
" updated_taxonomy = chain.invoke(\n",
" {\n",
" \"data_xml\": data_table_xml,\n",
" \"use_case\": configurable[\"use_case\"],\n",
" \"cluster_table_xml\": cluster_table_xml,\n",
" \"suggestion_length\": configurable.get(\"suggestion_length\", 30),\n",
" \"cluster_name_length\": configurable.get(\"cluster_name_length\", 10),\n",
" \"cluster_description_length\": configurable.get(\n",
" \"cluster_description_length\", 30\n",
" ),\n",
" \"explanation_length\": configurable.get(\"explanation_length\", 20),\n",
" \"max_num_clusters\": configurable.get(\"max_num_clusters\", 25),\n",
" }\n",
" )\n",
"\n",
" return {\n",
" \"clusters\": [updated_taxonomy[\"clusters\"]],\n",
" }"
]
"source": ["from typing import Dict\n\nfrom langchain_core.runnables import Runnable\n\n\ndef parse_taxa(output_text: str) -> Dict:\n \"\"\"Extract the taxonomy from the generated output.\"\"\"\n cluster_matches = re.findall(\n r\"\\s*<id>(.*?)</id>\\s*<name>(.*?)</name>\\s*<description>(.*?)</description>\\s*\",\n output_text,\n re.DOTALL,\n )\n clusters = [\n {\"id\": id.strip(), \"name\": name.strip(), \"description\": description.strip()}\n for id, name, description in cluster_matches\n ]\n # We don't parse the explanation since it isn't used downstream\n return {\"clusters\": clusters}\n\n\ndef format_docs(docs: List[Doc]) -> str:\n xml_table = \"<conversations>\\n\"\n for doc in docs:\n xml_table += f'<conv_summ id={doc[\"id\"]}>{doc[\"summary\"]}</conv_summ>\\n'\n xml_table += \"</conversations>\"\n return xml_table\n\n\ndef format_taxonomy(clusters):\n xml = \"<cluster_table>\\n\"\n for label in clusters:\n xml += \" <cluster>\\n\"\n xml += f' <id>{label[\"id\"]}</id>\\n'\n xml += f' <name>{label[\"name\"]}</name>\\n'\n xml += f' <description>{label[\"description\"]}</description>\\n'\n xml += \" </cluster>\\n\"\n xml += \"</cluster_table>\"\n return xml\n\n\ndef invoke_taxonomy_chain(\n chain: Runnable,\n state: TaxonomyGenerationState,\n config: RunnableConfig,\n mb_indices: List[int],\n) -> TaxonomyGenerationState:\n configurable = config[\"configurable\"]\n docs = state[\"documents\"]\n minibatch = [docs[idx] for idx in mb_indices]\n data_table_xml = format_docs(minibatch)\n\n previous_taxonomy = state[\"clusters\"][-1] if state[\"clusters\"] else []\n cluster_table_xml = format_taxonomy(previous_taxonomy)\n\n updated_taxonomy = chain.invoke(\n {\n \"data_xml\": data_table_xml,\n \"use_case\": configurable[\"use_case\"],\n \"cluster_table_xml\": cluster_table_xml,\n \"suggestion_length\": configurable.get(\"suggestion_length\", 30),\n \"cluster_name_length\": configurable.get(\"cluster_name_length\", 10),\n \"cluster_description_length\": configurable.get(\n \"cluster_description_length\", 30\n ),\n \"explanation_length\": configurable.get(\"explanation_length\", 20),\n \"max_num_clusters\": configurable.get(\"max_num_clusters\", 25),\n }\n )\n\n return {\n \"clusters\": [updated_taxonomy[\"clusters\"]],\n }"]
},
{
"cell_type": "markdown",
@@ -350,34 +136,7 @@
"id": "553dff30-ce53-47d8-ab3c-d2f437b7d5f4",
"metadata": {},
"outputs": [],
"source": [
"# We will share an LLM for each step of the generate -> update -> review cycle\n",
"# You may want to consider using Opus or another more powerful model for this\n",
"taxonomy_generation_llm = ChatAnthropic(\n",
" model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000\n",
")\n",
"\n",
"\n",
"## Initial generation\n",
"taxonomy_generation_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-generation\").partial(\n",
" use_case=\"Generate the taxonomy that can be used to label the user intent in the conversation.\",\n",
")\n",
"\n",
"taxa_gen_llm_chain = (\n",
" taxonomy_generation_prompt | taxonomy_generation_llm | StrOutputParser()\n",
").with_config(run_name=\"GenerateTaxonomy\")\n",
"\n",
"\n",
"generate_taxonomy_chain = taxa_gen_llm_chain | parse_taxa\n",
"\n",
"\n",
"def generate_taxonomy(\n",
" state: TaxonomyGenerationState, config: RunnableConfig\n",
") -> TaxonomyGenerationState:\n",
" return invoke_taxonomy_chain(\n",
" generate_taxonomy_chain, state, config, state[\"minibatches\"][0]\n",
" )"
]
"source": ["# We will share an LLM for each step of the generate -> update -> review cycle\n# You may want to consider using Opus or another more powerful model for this\ntaxonomy_generation_llm = ChatAnthropic(\n model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000\n)\n\n\n## Initial generation\ntaxonomy_generation_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-generation\").partial(\n use_case=\"Generate the taxonomy that can be used to label the user intent in the conversation.\",\n)\n\ntaxa_gen_llm_chain = (\n taxonomy_generation_prompt | taxonomy_generation_llm | StrOutputParser()\n).with_config(run_name=\"GenerateTaxonomy\")\n\n\ngenerate_taxonomy_chain = taxa_gen_llm_chain | parse_taxa\n\n\ndef generate_taxonomy(\n state: TaxonomyGenerationState, config: RunnableConfig\n) -> TaxonomyGenerationState:\n return invoke_taxonomy_chain(\n generate_taxonomy_chain, state, config, state[\"minibatches\"][0]\n )"]
},
{
"cell_type": "markdown",
@@ -395,25 +154,7 @@
"id": "b8739b5b-ba8a-4c40-bd25-a3b06a19949d",
"metadata": {},
"outputs": [],
"source": [
"taxonomy_update_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-update\")\n",
"\n",
"taxa_update_llm_chain = (\n",
" taxonomy_update_prompt | taxonomy_generation_llm | StrOutputParser()\n",
").with_config(run_name=\"UpdateTaxonomy\")\n",
"\n",
"\n",
"update_taxonomy_chain = taxa_update_llm_chain | parse_taxa\n",
"\n",
"\n",
"def update_taxonomy(\n",
" state: TaxonomyGenerationState, config: RunnableConfig\n",
") -> TaxonomyGenerationState:\n",
" which_mb = len(state[\"clusters\"]) % len(state[\"minibatches\"])\n",
" return invoke_taxonomy_chain(\n",
" update_taxonomy_chain, state, config, state[\"minibatches\"][which_mb]\n",
" )"
]
"source": ["taxonomy_update_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-update\")\n\ntaxa_update_llm_chain = (\n taxonomy_update_prompt | taxonomy_generation_llm | StrOutputParser()\n).with_config(run_name=\"UpdateTaxonomy\")\n\n\nupdate_taxonomy_chain = taxa_update_llm_chain | parse_taxa\n\n\ndef update_taxonomy(\n state: TaxonomyGenerationState, config: RunnableConfig\n) -> TaxonomyGenerationState:\n which_mb = len(state[\"clusters\"]) % len(state[\"minibatches\"])\n return invoke_taxonomy_chain(\n update_taxonomy_chain, state, config, state[\"minibatches\"][which_mb]\n )"]
},
{
"cell_type": "markdown",
@@ -431,28 +172,7 @@
"id": "0039cf1c-54d5-4e9e-8dd6-a5cebfaec92d",
"metadata": {},
"outputs": [],
"source": [
"taxonomy_review_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-review\")\n",
"\n",
"taxa_review_llm_chain = (\n",
" taxonomy_review_prompt | taxonomy_generation_llm | StrOutputParser()\n",
").with_config(run_name=\"ReviewTaxonomy\")\n",
"\n",
"\n",
"review_taxonomy_chain = taxa_review_llm_chain | parse_taxa\n",
"\n",
"\n",
"def review_taxonomy(\n",
" state: TaxonomyGenerationState, config: RunnableConfig\n",
") -> TaxonomyGenerationState:\n",
" batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n",
" original = state[\"documents\"]\n",
" indices = list(range(len(original)))\n",
" random.shuffle(indices)\n",
" return invoke_taxonomy_chain(\n",
" review_taxonomy_chain, state, config, indices[:batch_size]\n",
" )"
]
"source": ["taxonomy_review_prompt = hub.pull(\"wfh/tnt-llm-taxonomy-review\")\n\ntaxa_review_llm_chain = (\n taxonomy_review_prompt | taxonomy_generation_llm | StrOutputParser()\n).with_config(run_name=\"ReviewTaxonomy\")\n\n\nreview_taxonomy_chain = taxa_review_llm_chain | parse_taxa\n\n\ndef review_taxonomy(\n state: TaxonomyGenerationState, config: RunnableConfig\n) -> TaxonomyGenerationState:\n batch_size = config[\"configurable\"].get(\"batch_size\", 200)\n original = state[\"documents\"]\n indices = list(range(len(original)))\n random.shuffle(indices)\n return invoke_taxonomy_chain(\n review_taxonomy_chain, state, config, indices[:batch_size]\n )"]
},
{
"cell_type": "markdown",
@@ -470,40 +190,7 @@
"id": "f1f97ea4-53e5-4f55-8d73-b5b2234a47d9",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph\n",
"\n",
"graph = StateGraph(TaxonomyGenerationState)\n",
"graph.add_node(\"summarize\", map_reduce_chain)\n",
"graph.add_node(\"get_minibatches\", get_minibatches)\n",
"graph.add_node(\"generate_taxonomy\", generate_taxonomy)\n",
"graph.add_node(\"update_taxonomy\", update_taxonomy)\n",
"graph.add_node(\"review_taxonomy\", review_taxonomy)\n",
"\n",
"graph.add_edge(\"summarize\", \"get_minibatches\")\n",
"graph.add_edge(\"get_minibatches\", \"generate_taxonomy\")\n",
"graph.add_edge(\"generate_taxonomy\", \"update_taxonomy\")\n",
"\n",
"\n",
"def should_review(state: TaxonomyGenerationState) -> str:\n",
" num_minibatches = len(state[\"minibatches\"])\n",
" num_revisions = len(state[\"clusters\"])\n",
" if num_revisions < num_minibatches:\n",
" return \"update_taxonomy\"\n",
" return \"review_taxonomy\"\n",
"\n",
"\n",
"graph.add_conditional_edges(\n",
" \"update_taxonomy\",\n",
" should_review,\n",
" # Optional (but required for the diagram to be drawn correctly below)\n",
" {\"update_taxonomy\": \"update_taxonomy\", \"review_taxonomy\": \"review_taxonomy\"},\n",
")\n",
"graph.set_finish_point(\"review_taxonomy\")\n",
"\n",
"graph.set_entry_point(\"summarize\")\n",
"app = graph.compile()"
]
"source": ["from langgraph.graph import StateGraph, START\n\ngraph = StateGraph(TaxonomyGenerationState)\ngraph.add_node(\"summarize\", map_reduce_chain)\ngraph.add_node(\"get_minibatches\", get_minibatches)\ngraph.add_node(\"generate_taxonomy\", generate_taxonomy)\ngraph.add_node(\"update_taxonomy\", update_taxonomy)\ngraph.add_node(\"review_taxonomy\", review_taxonomy)\n\ngraph.add_edge(\"summarize\", \"get_minibatches\")\ngraph.add_edge(\"get_minibatches\", \"generate_taxonomy\")\ngraph.add_edge(\"generate_taxonomy\", \"update_taxonomy\")\n\n\ndef should_review(state: TaxonomyGenerationState) -> str:\n num_minibatches = len(state[\"minibatches\"])\n num_revisions = len(state[\"clusters\"])\n if num_revisions < num_minibatches:\n return \"update_taxonomy\"\n return \"review_taxonomy\"\n\n\ngraph.add_conditional_edges(\n \"update_taxonomy\",\n should_review,\n # Optional (but required for the diagram to be drawn correctly below)\n {\"update_taxonomy\": \"update_taxonomy\", \"review_taxonomy\": \"review_taxonomy\"},\n)\ngraph.set_finish_point(\"review_taxonomy\")\n\ngraph.add_edge(START, \"summarize\")\napp = graph.compile()"]
},
{
"cell_type": "code",
@@ -523,11 +210,7 @@
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import Image\n",
"\n",
"Image(app.get_graph().draw_png())"
]
"source": ["from IPython.display import Image\n\nImage(app.get_graph().draw_png())"]
},
{
"cell_type": "markdown",
@@ -549,51 +232,7 @@
"id": "bcc65649-157f-4848-9ef0-8a9932a98d85",
"metadata": {},
"outputs": [],
"source": [
"from datetime import datetime, timedelta\n",
"\n",
"from langsmith import Client\n",
"\n",
"project_name = \"YOUR PROJECT NAME\" # Update to your own project\n",
"client = Client()\n",
"\n",
"past_week = datetime.now() - timedelta(days=7)\n",
"runs = list(\n",
" client.list_runs(\n",
" project_name=project_name,\n",
" filter=\"eq(is_root, true)\",\n",
" start_time=past_week,\n",
" # We only need to return the inputs + outputs\n",
" select=[\"inputs\", \"outputs\"],\n",
" )\n",
")\n",
"\n",
"\n",
"# Convert the langsmith traces to our graph's Doc object.\n",
"def run_to_doc(run) -> Doc:\n",
" turns = []\n",
" idx = 0\n",
" for turn in run.inputs.get(\"chat_history\") or []:\n",
" key, value = next(iter(turn.items()))\n",
" turns.append(f\"<{key} idx={idx}>\\n{value}\\n</{key}>\")\n",
" idx += 1\n",
" turns.append(\n",
" f\"\"\"\n",
"<human idx={idx}>\n",
"{run.inputs['question']}\n",
"</human>\"\"\"\n",
" )\n",
" if run.outputs and run.outputs[\"output\"]:\n",
" turns.append(\n",
" f\"\"\"<ai idx={idx+1}>\n",
"{run.outputs['output']}\n",
"</ai>\"\"\"\n",
" )\n",
" return {\n",
" \"id\": str(run.id),\n",
" \"content\": (\"\\n\".join(turns)),\n",
" }"
]
"source": ["from datetime import datetime, timedelta\n\nfrom langsmith import Client\n\nproject_name = \"YOUR PROJECT NAME\" # Update to your own project\nclient = Client()\n\npast_week = datetime.now() - timedelta(days=7)\nruns = list(\n client.list_runs(\n project_name=project_name,\n filter=\"eq(is_root, true)\",\n start_time=past_week,\n # We only need to return the inputs + outputs\n select=[\"inputs\", \"outputs\"],\n )\n)\n\n\n# Convert the langsmith traces to our graph's Doc object.\ndef run_to_doc(run) -> Doc:\n turns = []\n idx = 0\n for turn in run.inputs.get(\"chat_history\") or []:\n key, value = next(iter(turn.items()))\n turns.append(f\"<{key} idx={idx}>\\n{value}\\n</{key}>\")\n idx += 1\n turns.append(\n f\"\"\"\n<human idx={idx}>\n{run.inputs['question']}\n</human>\"\"\"\n )\n if run.outputs and run.outputs[\"output\"]:\n turns.append(\n f\"\"\"<ai idx={idx+1}>\n{run.outputs['output']}\n</ai>\"\"\"\n )\n return {\n \"id\": str(run.id),\n \"content\": (\"\\n\".join(turns)),\n }"]
},
{
"cell_type": "markdown",
@@ -611,15 +250,7 @@
"id": "900906b5-9264-46a8-ba83-46307f8c25d0",
"metadata": {},
"outputs": [],
"source": [
"from langchain.cache import InMemoryCache\n",
"from langchain.globals import set_llm_cache\n",
"\n",
"# Optional. If you are running into errors or rate limits and want to avoid repeated computation,\n",
"# you can set this while debugging\n",
"\n",
"set_llm_cache(InMemoryCache())"
]
"source": ["from langchain.cache import InMemoryCache\nfrom langchain.globals import set_llm_cache\n\n# Optional. If you are running into errors or rate limits and want to avoid repeated computation,\n# you can set this while debugging\n\nset_llm_cache(InMemoryCache())"]
},
{
"cell_type": "code",
@@ -627,39 +258,7 @@
"id": "c2340177-f40c-407a-8e3e-cb06c2ef09ce",
"metadata": {},
"outputs": [],
"source": [
"# We will randomly sample down to 1K docs to speed things up\n",
"docs = [run_to_doc(run) for run in runs if run.inputs]\n",
"docs = random.sample(docs, min(len(docs), 1000))\n",
"use_case = (\n",
" \"Generate the taxonomy that can be used both to label the user intent\"\n",
" \" as well as to identify any required documentation (references, how-tos, etc.)\"\n",
" \" that would benefit the user.\"\n",
")\n",
"\n",
"stream = app.stream(\n",
" {\"documents\": docs},\n",
" {\n",
" \"configurable\": {\n",
" \"use_case\": use_case,\n",
" # Optional:\n",
" \"batch_size\": 400,\n",
" \"suggestion_length\": 30,\n",
" \"cluster_name_length\": 10,\n",
" \"cluster_description_length\": 30,\n",
" \"explanation_length\": 20,\n",
" \"max_num_clusters\": 25,\n",
" },\n",
" # We batch summarize the docs. To avoid getting errors, we will limit the\n",
" # degree of parallelism to permit.\n",
" \"max_concurrency\": 2,\n",
" },\n",
")\n",
"\n",
"for step in stream:\n",
" node, state = next(iter(step.items()))\n",
" print(node, str(state)[:20] + \" ...\")"
]
"source": ["# We will randomly sample down to 1K docs to speed things up\ndocs = [run_to_doc(run) for run in runs if run.inputs]\ndocs = random.sample(docs, min(len(docs), 1000))\nuse_case = (\n \"Generate the taxonomy that can be used both to label the user intent\"\n \" as well as to identify any required documentation (references, how-tos, etc.)\"\n \" that would benefit the user.\"\n)\n\nstream = app.stream(\n {\"documents\": docs},\n {\n \"configurable\": {\n \"use_case\": use_case,\n # Optional:\n \"batch_size\": 400,\n \"suggestion_length\": 30,\n \"cluster_name_length\": 10,\n \"cluster_description_length\": 30,\n \"explanation_length\": 20,\n \"max_num_clusters\": 25,\n },\n # We batch summarize the docs. To avoid getting errors, we will limit the\n # degree of parallelism to permit.\n \"max_concurrency\": 2,\n },\n)\n\nfor step in stream:\n node, state = next(iter(step.items()))\n print(node, str(state)[:20] + \" ...\")"]
},
{
"cell_type": "markdown",
@@ -719,31 +318,7 @@
"output_type": "execute_result"
}
],
"source": [
"from IPython.display import Markdown\n",
"\n",
"\n",
"def format_taxonomy_md(clusters):\n",
" md = \"## Final Taxonomy\\n\\n\"\n",
" md += \"| ID | Name | Description |\\n\"\n",
" md += \"|----|------|-------------|\\n\"\n",
"\n",
" # Fill the table with cluster data\n",
" for label in clusters:\n",
" id = label[\"id\"]\n",
" name = label[\"name\"].replace(\n",
" \"|\", \"\\\\|\"\n",
" ) # Escape any pipe characters within the content\n",
" description = label[\"description\"].replace(\n",
" \"|\", \"\\\\|\"\n",
" ) # Escape any pipe characters\n",
" md += f\"| {id} | {name} | {description} |\\n\"\n",
"\n",
" return md\n",
"\n",
"\n",
"Markdown(format_taxonomy_md(step[\"__end__\"][\"clusters\"][-1]))"
]
"source": ["from IPython.display import Markdown\n\n\ndef format_taxonomy_md(clusters):\n md = \"## Final Taxonomy\\n\\n\"\n md += \"| ID | Name | Description |\\n\"\n md += \"|----|------|-------------|\\n\"\n\n # Fill the table with cluster data\n for label in clusters:\n id = label[\"id\"]\n name = label[\"name\"].replace(\n \"|\", \"\\\\|\"\n ) # Escape any pipe characters within the content\n description = label[\"description\"].replace(\n \"|\", \"\\\\|\"\n ) # Escape any pipe characters\n md += f\"| {id} | {name} | {description} |\\n\"\n\n return md\n\n\nMarkdown(format_taxonomy_md(step[\"__end__\"][\"clusters\"][-1]))"]
},
{
"cell_type": "markdown",
@@ -773,32 +348,7 @@
"id": "8aa8a6f5-f53a-41e5-b09d-c6e8476e5471",
"metadata": {},
"outputs": [],
"source": [
"labeling_prompt = hub.pull(\"wfh/tnt-llm-classify\")\n",
"\n",
"labeling_llm = ChatAnthropic(model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000)\n",
"labeling_llm_chain = (labeling_prompt | labeling_llm | StrOutputParser()).with_config(\n",
" run_name=\"ClassifyDocs\"\n",
")\n",
"\n",
"\n",
"def parse_labels(output_text: str) -> Dict:\n",
" \"\"\"Parse the generated labels from the predictions.\"\"\"\n",
" category_matches = re.findall(\n",
" r\"\\s*<category>(.*?)</category>.*\",\n",
" output_text,\n",
" re.DOTALL,\n",
" )\n",
" categories = [{\"category\": category.strip()} for category in category_matches]\n",
" if len(categories) > 1:\n",
" logger.warning(f\"Multiple selected categories: {categories}\")\n",
" label = categories[0]\n",
" stripped = re.sub(r\"^\\d+\\.\\s*\", \"\", label[\"category\"]).strip()\n",
" return {\"category\": stripped}\n",
"\n",
"\n",
"labeling_chain = labeling_llm_chain | parse_labels"
]
"source": ["labeling_prompt = hub.pull(\"wfh/tnt-llm-classify\")\n\nlabeling_llm = ChatAnthropic(model=\"claude-3-haiku-20240307\", max_tokens_to_sample=2000)\nlabeling_llm_chain = (labeling_prompt | labeling_llm | StrOutputParser()).with_config(\n run_name=\"ClassifyDocs\"\n)\n\n\ndef parse_labels(output_text: str) -> Dict:\n \"\"\"Parse the generated labels from the predictions.\"\"\"\n category_matches = re.findall(\n r\"\\s*<category>(.*?)</category>.*\",\n output_text,\n re.DOTALL,\n )\n categories = [{\"category\": category.strip()} for category in category_matches]\n if len(categories) > 1:\n logger.warning(f\"Multiple selected categories: {categories}\")\n label = categories[0]\n stripped = re.sub(r\"^\\d+\\.\\s*\", \"\", label[\"category\"]).strip()\n return {\"category\": stripped}\n\n\nlabeling_chain = labeling_llm_chain | parse_labels"]
},
{
"cell_type": "code",
@@ -806,23 +356,7 @@
"id": "59c06eea-ecbf-43af-a292-71816ccd92b8",
"metadata": {},
"outputs": [],
"source": [
"final_taxonomy = step[\"__end__\"][\"clusters\"][-1]\n",
"xml_taxonomy = format_taxonomy(final_taxonomy)\n",
"results = labeling_chain.batch(\n",
" [\n",
" {\n",
" \"content\": doc[\"content\"],\n",
" \"taxonomy\": xml_taxonomy,\n",
" }\n",
" for doc in docs\n",
" ],\n",
" {\"max_concurrency\": 5},\n",
" return_exceptions=True,\n",
")\n",
"# Update the docs to include the categories\n",
"updated_docs = [{**doc, **category} for doc, category in zip(docs, results)]"
]
"source": ["final_taxonomy = step[\"__end__\"][\"clusters\"][-1]\nxml_taxonomy = format_taxonomy(final_taxonomy)\nresults = labeling_chain.batch(\n [\n {\n \"content\": doc[\"content\"],\n \"taxonomy\": xml_taxonomy,\n }\n for doc in docs\n ],\n {\"max_concurrency\": 5},\n return_exceptions=True,\n)\n# Update the docs to include the categories\nupdated_docs = [{**doc, **category} for doc, category in zip(docs, results)]"]
},
{
"cell_type": "code",
@@ -830,10 +364,7 @@
"id": "0ef9be82-278e-4501-8af9-70409ce15cc2",
"metadata": {},
"outputs": [],
"source": [
"if \"OPENAI_API_KEY\" not in os.environ:\n",
" os.environ[\"OPENAI_API_KEY\"] = getpass(\"Enter your OPENAI_API_KEY: \")"
]
"source": ["if \"OPENAI_API_KEY\" not in os.environ:\n os.environ[\"OPENAI_API_KEY\"] = getpass(\"Enter your OPENAI_API_KEY: \")"]
},
{
"cell_type": "code",
@@ -841,14 +372,7 @@
"id": "c21f787e-2dcb-49c2-9cc1-5284a1732fbc",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"# Consider using other embedding models here too!\n",
"encoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\n",
"vectors = encoder.embed_documents([doc[\"content\"] for doc in docs])\n",
"embedded_docs = [{**doc, \"embedding\": v} for doc, v in zip(updated_docs, vectors)]"
]
"source": ["from langchain_openai import OpenAIEmbeddings\n\n# Consider using other embedding models here too!\nencoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\nvectors = encoder.embed_documents([doc[\"content\"] for doc in docs])\nembedded_docs = [{**doc, \"embedding\": v} for doc, v in zip(updated_docs, vectors)]"]
},
{
"cell_type": "markdown",
@@ -877,51 +401,7 @@
]
}
],
"source": [
"import numpy as np\n",
"from sklearn.linear_model import LogisticRegression\n",
"from sklearn.metrics import accuracy_score, f1_score\n",
"from sklearn.model_selection import train_test_split\n",
"from sklearn.utils import class_weight\n",
"\n",
"# Create a dictionary mapping category names to their indices in the taxonomy\n",
"category_to_index = {d[\"name\"]: i for i, d in enumerate(final_taxonomy)}\n",
"category_to_index[\"Other\"] = len(category_to_index)\n",
"# Convert category strings to numeric labels\n",
"labels = [\n",
" category_to_index.get(d[\"category\"], category_to_index[\"Other\"])\n",
" for d in embedded_docs\n",
"]\n",
"\n",
"label_vectors = [d[\"embedding\"] for d in embedded_docs]\n",
"\n",
"X_train, X_test, y_train, y_test = train_test_split(\n",
" label_vectors, labels, test_size=0.2, random_state=42\n",
")\n",
"\n",
"# Calculate class weights\n",
"class_weights = class_weight.compute_class_weight(\n",
" class_weight=\"balanced\", classes=np.unique(y_train), y=y_train\n",
")\n",
"class_weight_dict = dict(enumerate(class_weights))\n",
"\n",
"# Weight the classes to partially handle imbalanced data\n",
"model = LogisticRegression(class_weight=class_weight_dict)\n",
"model.fit(X_train, y_train)\n",
"\n",
"train_preds = model.predict(X_train)\n",
"test_preds = model.predict(X_test)\n",
"\n",
"train_acc = accuracy_score(y_train, train_preds)\n",
"test_acc = accuracy_score(y_test, test_preds)\n",
"train_f1 = f1_score(y_train, train_preds, average=\"weighted\")\n",
"test_f1 = f1_score(y_test, test_preds, average=\"weighted\")\n",
"\n",
"print(f\"Train Accuracy: {train_acc:.3f}\")\n",
"print(f\"Test Accuracy: {test_acc:.3f}\")\n",
"print(f\"Train F1 Score: {train_f1:.3f}\")\n",
"print(f\"Test F1 Score: {test_f1:.3f}\")"
]
"source": ["import numpy as np\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import accuracy_score, f1_score\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils import class_weight\n\n# Create a dictionary mapping category names to their indices in the taxonomy\ncategory_to_index = {d[\"name\"]: i for i, d in enumerate(final_taxonomy)}\ncategory_to_index[\"Other\"] = len(category_to_index)\n# Convert category strings to numeric labels\nlabels = [\n category_to_index.get(d[\"category\"], category_to_index[\"Other\"])\n for d in embedded_docs\n]\n\nlabel_vectors = [d[\"embedding\"] for d in embedded_docs]\n\nX_train, X_test, y_train, y_test = train_test_split(\n label_vectors, labels, test_size=0.2, random_state=42\n)\n\n# Calculate class weights\nclass_weights = class_weight.compute_class_weight(\n class_weight=\"balanced\", classes=np.unique(y_train), y=y_train\n)\nclass_weight_dict = dict(enumerate(class_weights))\n\n# Weight the classes to partially handle imbalanced data\nmodel = LogisticRegression(class_weight=class_weight_dict)\nmodel.fit(X_train, y_train)\n\ntrain_preds = model.predict(X_train)\ntest_preds = model.predict(X_test)\n\ntrain_acc = accuracy_score(y_train, train_preds)\ntest_acc = accuracy_score(y_test, test_preds)\ntrain_f1 = f1_score(y_train, train_preds, average=\"weighted\")\ntest_f1 = f1_score(y_test, test_preds, average=\"weighted\")\n\nprint(f\"Train Accuracy: {train_acc:.3f}\")\nprint(f\"Test Accuracy: {test_acc:.3f}\")\nprint(f\"Train F1 Score: {train_f1:.3f}\")\nprint(f\"Test F1 Score: {test_f1:.3f}\")"]
},
{
"cell_type": "markdown",
@@ -939,15 +419,7 @@
"id": "c27cbb6b-4d0f-476a-bef3-31ed307ce45f",
"metadata": {},
"outputs": [],
"source": [
"from joblib import dump as jl_dump\n",
"\n",
"categories = list(category_to_index)\n",
"\n",
"# Save the model and categories to a file\n",
"with open(\"model.joblib\", \"wb\") as file:\n",
" jl_dump((model, categories), file)"
]
"source": ["from joblib import dump as jl_dump\n\ncategories = list(category_to_index)\n\n# Save the model and categories to a file\nwith open(\"model.joblib\", \"wb\") as file:\n jl_dump((model, categories), file)"]
},
{
"cell_type": "markdown",
@@ -965,24 +437,7 @@
"id": "28f0b88a-b308-4208-b482-6c157357dfc6",
"metadata": {},
"outputs": [],
"source": [
"from joblib import load as jl_load\n",
"from langchain_openai import OpenAIEmbeddings\n",
"\n",
"loaded_model, loaded_categories = jl_load(\"model.joblib\")\n",
"encoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\n",
"\n",
"\n",
"def get_category_name(predictions):\n",
" return [loaded_categories[pred] for pred in predictions]\n",
"\n",
"\n",
"classifier = (\n",
" RunnableLambda(encoder.embed_documents, encoder.aembed_documents)\n",
" | loaded_model.predict\n",
" | get_category_name\n",
")"
]
"source": ["from joblib import load as jl_load\nfrom langchain_openai import OpenAIEmbeddings\n\nloaded_model, loaded_categories = jl_load(\"model.joblib\")\nencoder = OpenAIEmbeddings(model=\"text-embedding-3-large\")\n\n\ndef get_category_name(predictions):\n return [loaded_categories[pred] for pred in predictions]\n\n\nclassifier = (\n RunnableLambda(encoder.embed_documents, encoder.aembed_documents)\n | loaded_model.predict\n | get_category_name\n)"]
},
{
"cell_type": "markdown",
@@ -1000,22 +455,7 @@
"id": "6cdb9d8a-2aa1-4f48-8b23-f311fdf36416",
"metadata": {},
"outputs": [],
"source": [
"client = Client()\n",
"\n",
"past_5_min = datetime.now() - timedelta(minutes=5)\n",
"runs = list(\n",
" client.list_runs(\n",
" project_name=project_name,\n",
" filter=\"eq(is_root, true)\",\n",
" start_time=past_5_min,\n",
" # We only need to return the inputs + outputs\n",
" select=[\"inputs\", \"outputs\"],\n",
" limit=100,\n",
" )\n",
")\n",
"docs = [run_to_doc(r) for r in runs]"
]
"source": ["client = Client()\n\npast_5_min = datetime.now() - timedelta(minutes=5)\nruns = list(\n client.list_runs(\n project_name=project_name,\n filter=\"eq(is_root, true)\",\n start_time=past_5_min,\n # We only need to return the inputs + outputs\n select=[\"inputs\", \"outputs\"],\n limit=100,\n )\n)\ndocs = [run_to_doc(r) for r in runs]"]
},
{
"cell_type": "code",
@@ -1038,10 +478,7 @@
]
}
],
"source": [
"classes = classifier.invoke([doc[\"content\"] for doc in docs])\n",
"print(classes[:2])"
]
"source": ["classes = classifier.invoke([doc[\"content\"] for doc in docs])\nprint(classes[:2])"]
},
{
"cell_type": "markdown",
File diff suppressed because it is too large Load Diff
+9 -120
View File
@@ -16,10 +16,7 @@
"id": "32a0e7f4",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph"
]
"source": ["%%capture --no-stderr\n%pip install -U langgraph"]
},
{
"cell_type": "markdown",
@@ -37,73 +34,7 @@
"id": "6d604311",
"metadata": {},
"outputs": [],
"source": [
"import random\n",
"from typing import Annotated, Literal\n",
"\n",
"from typing_extensions import TypedDict\n",
"\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"\n",
"class State(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"\n",
"class MyNode:\n",
" def __init__(self, name: str):\n",
" self.name = name\n",
"\n",
" def __call__(self, state: State):\n",
" return {\"messages\": [(\"assistant\", f\"Called node {self.name}\")]}\n",
"\n",
"\n",
"def route(state) -> Literal[\"entry_node\", \"__end__\"]:\n",
" if len(state[\"messages\"]) > 10:\n",
" return \"__end__\"\n",
" return \"entry_node\"\n",
"\n",
"\n",
"def add_fractal_nodes(builder, current_node, level, max_level):\n",
" if level > max_level:\n",
" return\n",
"\n",
" # Number of nodes to create at this level\n",
" num_nodes = random.randint(1, 3) # Adjust randomness as needed\n",
" for i in range(num_nodes):\n",
" nm = [\"A\", \"B\", \"C\"][i]\n",
" node_name = f\"node_{current_node}_{nm}\"\n",
" builder.add_node(node_name, MyNode(node_name))\n",
" builder.add_edge(current_node, node_name)\n",
"\n",
" # Recursively add more nodes\n",
" r = random.random()\n",
" if r > 0.2 and level + 1 < max_level:\n",
" add_fractal_nodes(builder, node_name, level + 1, max_level)\n",
" elif r > 0.05:\n",
" builder.add_conditional_edges(node_name, route, node_name)\n",
" else:\n",
" # End\n",
" builder.add_edge(node_name, \"__end__\")\n",
"\n",
"\n",
"def build_fractal_graph(max_level: int):\n",
" builder = StateGraph(State)\n",
" entry_point = \"entry_node\"\n",
" builder.add_node(entry_point, MyNode(entry_point))\n",
" builder.set_entry_point(entry_point)\n",
"\n",
" add_fractal_nodes(builder, entry_point, 1, max_level)\n",
"\n",
" # Optional: set a finish point if required\n",
" builder.set_finish_point(entry_point) # or any specific node\n",
"\n",
" return builder.compile()\n",
"\n",
"\n",
"app = build_fractal_graph(3)"
]
"source": ["import random\nfrom typing import Annotated, Literal\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\nclass MyNode:\n def __init__(self, name: str):\n self.name = name\n\n def __call__(self, state: State):\n return {\"messages\": [(\"assistant\", f\"Called node {self.name}\")]}\n\n\ndef route(state) -> Literal[\"entry_node\", \"__end__\"]:\n if len(state[\"messages\"]) > 10:\n return \"__end__\"\n return \"entry_node\"\n\n\ndef add_fractal_nodes(builder, current_node, level, max_level):\n if level > max_level:\n return\n\n # Number of nodes to create at this level\n num_nodes = random.randint(1, 3) # Adjust randomness as needed\n for i in range(num_nodes):\n nm = [\"A\", \"B\", \"C\"][i]\n node_name = f\"node_{current_node}_{nm}\"\n builder.add_node(node_name, MyNode(node_name))\n builder.add_edge(current_node, node_name)\n\n # Recursively add more nodes\n r = random.random()\n if r > 0.2 and level + 1 < max_level:\n add_fractal_nodes(builder, node_name, level + 1, max_level)\n elif r > 0.05:\n builder.add_conditional_edges(node_name, route, node_name)\n else:\n # End\n builder.add_edge(node_name, \"__end__\")\n\n\ndef build_fractal_graph(max_level: int):\n builder = StateGraph(State)\n entry_point = \"entry_node\"\n builder.add_node(entry_point, MyNode(entry_point))\n builder.add_edge(START, entry_point)\n\n add_fractal_nodes(builder, entry_point, 1, max_level)\n\n # Optional: set a finish point if required\n builder.set_finish_point(entry_point) # or any specific node\n\n return builder.compile()\n\n\napp = build_fractal_graph(3)"]
},
{
"cell_type": "markdown",
@@ -165,9 +96,7 @@
]
}
],
"source": [
"app.get_graph().print_ascii()"
]
"source": ["app.get_graph().print_ascii()"]
},
{
"cell_type": "markdown",
@@ -226,9 +155,7 @@
]
}
],
"source": [
"print(app.get_graph().draw_mermaid())"
]
"source": ["print(app.get_graph().draw_mermaid())"]
},
{
"cell_type": "markdown",
@@ -266,18 +193,7 @@
"output_type": "display_data"
}
],
"source": [
"from IPython.display import Image, display\n",
"from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeColors\n",
"\n",
"display(\n",
" Image(\n",
" app.get_graph().draw_mermaid_png(\n",
" draw_method=MermaidDrawMethod.API,\n",
" )\n",
" )\n",
")"
]
"source": ["from IPython.display import Image, display\nfrom langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeColors\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n draw_method=MermaidDrawMethod.API,\n )\n )\n)"]
},
{
"cell_type": "markdown",
@@ -303,11 +219,7 @@
}
},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet pyppeteer\n",
"%pip install --quiet nest_asyncio"
]
"source": ["%%capture --no-stderr\n%pip install --quiet pyppeteer\n%pip install --quiet nest_asyncio"]
},
{
"cell_type": "code",
@@ -331,25 +243,7 @@
"output_type": "display_data"
}
],
"source": [
"import nest_asyncio\n",
"\n",
"nest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n",
"\n",
"display(\n",
" Image(\n",
" app.get_graph().draw_mermaid_png(\n",
" curve_style=CurveStyle.LINEAR,\n",
" node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n",
" wrap_label_n_words=9,\n",
" output_file_path=None,\n",
" draw_method=MermaidDrawMethod.PYPPETEER,\n",
" background_color=\"white\",\n",
" padding=10,\n",
" )\n",
" )\n",
")"
]
"source": ["import nest_asyncio\n\nnest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n curve_style=CurveStyle.LINEAR,\n node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n wrap_label_n_words=9,\n output_file_path=None,\n draw_method=MermaidDrawMethod.PYPPETEER,\n background_color=\"white\",\n padding=10,\n )\n )\n)"]
},
{
"cell_type": "markdown",
@@ -375,10 +269,7 @@
}
},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install pygraphviz"
]
"source": ["%%capture --no-stderr\n%pip install pygraphviz"]
},
{
"cell_type": "code",
@@ -402,9 +293,7 @@
"output_type": "display_data"
}
],
"source": [
"display(Image(app.get_graph().draw_png()))"
]
"source": ["display(Image(app.get_graph().draw_png()))"]
}
],
"metadata": {