diff --git a/examples/chatbots/customer-support.ipynb b/examples/chatbots/customer-support.ipynb
index 374d3e538..b37f4e396 100644
--- a/examples/chatbots/customer-support.ipynb
+++ b/examples/chatbots/customer-support.ipynb
@@ -32,7 +32,10 @@
"scrolled": true
},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-community langchain-openai scikit-learn"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph langchain-community langchain-openai scikit-learn"
+ ]
},
{
"cell_type": "markdown",
@@ -48,7 +51,15 @@
"id": "3d1ef253-6b0c-4481-868c-e1fe84f2c8ff",
"metadata": {},
"outputs": [],
- "source": ["import requests\n\nurl = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\nresponse = requests.get(url)\n\nwith open(\"Chinook.db\", \"wb\") as file:\n file.write(response.content)"]
+ "source": [
+ "import requests\n",
+ "\n",
+ "url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n",
+ "response = requests.get(url)\n",
+ "\n",
+ "with open(\"Chinook.db\", \"wb\") as file:\n",
+ " file.write(response.content)"
+ ]
},
{
"cell_type": "code",
@@ -77,7 +88,12 @@
"output_type": "execute_result"
}
],
- "source": ["from langchain_community.utilities import SQLDatabase\n\ndb = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\ndb.get_usable_table_names()"]
+ "source": [
+ "from langchain_community.utilities import SQLDatabase\n",
+ "\n",
+ "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n",
+ "db.get_usable_table_names()"
+ ]
},
{
"cell_type": "markdown",
@@ -96,7 +112,11 @@
"id": "d9ea4e80-30e6-4d46-b480-35f0be2fb055",
"metadata": {},
"outputs": [],
- "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"]
+ "source": [
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"
+ ]
},
{
"cell_type": "markdown",
@@ -118,7 +138,9 @@
"id": "ea958e9f-ab1f-49b5-bd85-16332055297c",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import HumanMessage, SystemMessage"]
+ "source": [
+ "from langchain_core.messages import HumanMessage, SystemMessage"
+ ]
},
{
"cell_type": "markdown",
@@ -137,7 +159,12 @@
"id": "975b039a",
"metadata": {},
"outputs": [],
- "source": ["# This tool is given to the agent to look up information about a customer\ndef get_customer_info(customer_id: int):\n \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"]
+ "source": [
+ "# This tool is given to the agent to look up information about a customer\n",
+ "def get_customer_info(customer_id: int):\n",
+ " \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n",
+ " return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"
+ ]
},
{
"cell_type": "code",
@@ -145,7 +172,20 @@
"id": "1d5fa446",
"metadata": {},
"outputs": [],
- "source": ["customer_prompt = \"\"\"Your job is to help a user update their profile.\n\nYou only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n\nIf you are unable to help the user, you can \"\"\"\n\n\ndef get_customer_messages(messages):\n return [SystemMessage(content=customer_prompt)] + messages\n\n\ncustomer_chain = get_customer_messages | model.bind_tools([get_customer_info])"]
+ "source": [
+ "customer_prompt = \"\"\"Your job is to help a user update their profile.\n",
+ "\n",
+ "You only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n",
+ "\n",
+ "If you are unable to help the user, you can \"\"\"\n",
+ "\n",
+ "\n",
+ "def get_customer_messages(messages):\n",
+ " return [SystemMessage(content=customer_prompt)] + messages\n",
+ "\n",
+ "\n",
+ "customer_chain = get_customer_messages | model.bind_tools([get_customer_info])"
+ ]
},
{
"cell_type": "markdown",
@@ -166,7 +206,19 @@
"id": "a8604a3b-b484-4b2b-a914-4236cb98c524",
"metadata": {},
"outputs": [],
- "source": ["from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_openai import OpenAIEmbeddings\n\nartists = db._execute(\"select * from Artist\")\nsongs = db._execute(\"select * from Track\")\nartist_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n).as_retriever()\nsong_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n).as_retriever()"]
+ "source": [
+ "from langchain_community.vectorstores import SKLearnVectorStore\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "\n",
+ "artists = db._execute(\"select * from Artist\")\n",
+ "songs = db._execute(\"select * from Track\")\n",
+ "artist_retriever = SKLearnVectorStore.from_texts(\n",
+ " [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n",
+ ").as_retriever()\n",
+ "song_retriever = SKLearnVectorStore.from_texts(\n",
+ " [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n",
+ ").as_retriever()"
+ ]
},
{
"cell_type": "markdown",
@@ -182,7 +234,16 @@
"id": "0a2a2b74",
"metadata": {},
"outputs": [],
- "source": ["def get_albums_by_artist(artist):\n \"\"\"Get albums by an artist (or similar artists).\"\"\"\n docs = artist_retriever.get_relevant_documents(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"]
+ "source": [
+ "def get_albums_by_artist(artist):\n",
+ " \"\"\"Get albums by an artist (or similar artists).\"\"\"\n",
+ " docs = artist_retriever.get_relevant_documents(artist)\n",
+ " artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n",
+ " return db.run(\n",
+ " f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n",
+ " include_columns=True,\n",
+ " )"
+ ]
},
{
"cell_type": "markdown",
@@ -198,7 +259,16 @@
"id": "da533f50",
"metadata": {},
"outputs": [],
- "source": ["def get_tracks_by_artist(artist):\n \"\"\"Get songs by an artist (or similar artists).\"\"\"\n docs = artist_retriever.invoke(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"]
+ "source": [
+ "def get_tracks_by_artist(artist):\n",
+ " \"\"\"Get songs by an artist (or similar artists).\"\"\"\n",
+ " docs = artist_retriever.invoke(artist)\n",
+ " artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n",
+ " return db.run(\n",
+ " f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n",
+ " include_columns=True,\n",
+ " )"
+ ]
},
{
"cell_type": "markdown",
@@ -214,7 +284,11 @@
"id": "b3c07010",
"metadata": {},
"outputs": [],
- "source": ["def check_for_songs(song_title):\n \"\"\"Check if a song exists by its name.\"\"\"\n return song_retriever.invoke(song_title)"]
+ "source": [
+ "def check_for_songs(song_title):\n",
+ " \"\"\"Check if a song exists by its name.\"\"\"\n",
+ " return song_retriever.invoke(song_title)"
+ ]
},
{
"cell_type": "markdown",
@@ -230,7 +304,23 @@
"id": "72a14d5c",
"metadata": {},
"outputs": [],
- "source": ["song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n\nYou only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n\nWhen looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\non similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n\n\ndef get_song_messages(messages):\n return [SystemMessage(content=song_system_message)] + messages\n\n\nsong_recc_chain = get_song_messages | model.bind_tools(\n [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n)"]
+ "source": [
+ "song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n",
+ "\n",
+ "You only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n",
+ "\n",
+ "When looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\n",
+ "on similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n",
+ "\n",
+ "\n",
+ "def get_song_messages(messages):\n",
+ " return [SystemMessage(content=song_system_message)] + messages\n",
+ "\n",
+ "\n",
+ "song_recc_chain = get_song_messages | model.bind_tools(\n",
+ " [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -249,7 +339,10 @@
"output_type": "execute_result"
}
],
- "source": ["msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\nsong_recc_chain.invoke(msgs)"]
+ "source": [
+ "msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\n",
+ "song_recc_chain.invoke(msgs)"
+ ]
},
{
"cell_type": "markdown",
@@ -267,7 +360,32 @@
"id": "73e74268",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Router(BaseModel):\n \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n\n choice: str = Field(description=\"should be one of: music, customer\")\n\n\nsystem_message = \"\"\"Your job is to help as a customer service representative for a music store.\n\nYou should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n\n- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n\nIf the user is asking or wants to ask about updating or accessing their information, send them to that route.\nIf the user is asking or wants to ask about music, send them to that route.\nOtherwise, respond.\"\"\"\n\n\ndef get_messages(messages):\n return [SystemMessage(content=system_message)] + messages"]
+ "source": [
+ "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "\n",
+ "\n",
+ "class Router(BaseModel):\n",
+ " \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n",
+ "\n",
+ " choice: str = Field(description=\"should be one of: music, customer\")\n",
+ "\n",
+ "\n",
+ "system_message = \"\"\"Your job is to help as a customer service representative for a music store.\n",
+ "\n",
+ "You should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n",
+ "\n",
+ "- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n",
+ "- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n",
+ "\n",
+ "If the user is asking or wants to ask about updating or accessing their information, send them to that route.\n",
+ "If the user is asking or wants to ask about music, send them to that route.\n",
+ "Otherwise, respond.\"\"\"\n",
+ "\n",
+ "\n",
+ "def get_messages(messages):\n",
+ " return [SystemMessage(content=system_message)] + messages"
+ ]
},
{
"cell_type": "code",
@@ -275,7 +393,9 @@
"id": "ddf27314",
"metadata": {},
"outputs": [],
- "source": ["chain = get_messages | model.bind_tools([Router])"]
+ "source": [
+ "chain = get_messages | model.bind_tools([Router])"
+ ]
},
{
"cell_type": "code",
@@ -294,7 +414,10 @@
"output_type": "execute_result"
}
],
- "source": ["msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\nchain.invoke(msgs)"]
+ "source": [
+ "msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\n",
+ "chain.invoke(msgs)"
+ ]
},
{
"cell_type": "code",
@@ -313,7 +436,10 @@
"output_type": "execute_result"
}
],
- "source": ["msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\nchain.invoke(msgs)"]
+ "source": [
+ "msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\n",
+ "chain.invoke(msgs)"
+ ]
},
{
"cell_type": "code",
@@ -321,7 +447,15 @@
"id": "bd6ddd8b-7500-46a7-811d-3bcb937bda51",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import AIMessage\n\n\ndef add_name(message, name):\n _dict = message.dict()\n _dict[\"name\"] = name\n return AIMessage(**_dict)"]
+ "source": [
+ "from langchain_core.messages import AIMessage\n",
+ "\n",
+ "\n",
+ "def add_name(message, name):\n",
+ " _dict = message.dict()\n",
+ " _dict[\"name\"] = name\n",
+ " return AIMessage(**_dict)"
+ ]
},
{
"cell_type": "code",
@@ -329,7 +463,45 @@
"id": "27494de5-8345-4c23-bc0e-81e0dd5d47d8",
"metadata": {},
"outputs": [],
- "source": ["import json\n\nfrom langgraph.graph import END, START\n\n\ndef _get_last_ai_message(messages):\n for m in messages[::-1]:\n if isinstance(m, AIMessage):\n return m\n return None\n\n\ndef _is_tool_call(msg):\n return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n\n\ndef _route(messages):\n last_message = messages[-1]\n if isinstance(last_message, AIMessage):\n if not last_message.tool_calls:\n return END\n else:\n if last_message.name == \"general\":\n if len(last_message.tool_calls) > 1:\n raise ValueError(\"Too many tools\")\n return last_message.tool_calls[0][\"args\"][\"choice\"]\n else:\n return \"tools\"\n last_m = _get_last_ai_message(messages)\n if last_m is None:\n return \"general\"\n if last_m.name == \"music\":\n return \"music\"\n elif last_m.name == \"customer\":\n return \"customer\"\n else:\n return \"general\""]
+ "source": [
+ "import json\n",
+ "\n",
+ "from langgraph.graph import END, START\n",
+ "\n",
+ "\n",
+ "def _get_last_ai_message(messages):\n",
+ " for m in messages[::-1]:\n",
+ " if isinstance(m, AIMessage):\n",
+ " return m\n",
+ " return None\n",
+ "\n",
+ "\n",
+ "def _is_tool_call(msg):\n",
+ " return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n",
+ "\n",
+ "\n",
+ "def _route(messages):\n",
+ " last_message = messages[-1]\n",
+ " if isinstance(last_message, AIMessage):\n",
+ " if not last_message.tool_calls:\n",
+ " return END\n",
+ " else:\n",
+ " if last_message.name == \"general\":\n",
+ " if len(last_message.tool_calls) > 1:\n",
+ " raise ValueError(\"Too many tools\")\n",
+ " return last_message.tool_calls[0][\"args\"][\"choice\"]\n",
+ " else:\n",
+ " return \"tools\"\n",
+ " last_m = _get_last_ai_message(messages)\n",
+ " if last_m is None:\n",
+ " return \"general\"\n",
+ " if last_m.name == \"music\":\n",
+ " return \"music\"\n",
+ " elif last_m.name == \"customer\":\n",
+ " return \"customer\"\n",
+ " else:\n",
+ " return \"general\""
+ ]
},
{
"cell_type": "code",
@@ -337,7 +509,12 @@
"id": "8aec704a-46fe-4fb3-bdee-11c3bbffc370",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.prebuilt import ToolNode\n\ntools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\ntool_node = ToolNode(tools)"]
+ "source": [
+ "from langgraph.prebuilt import ToolNode\n",
+ "\n",
+ "tools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\n",
+ "tool_node = ToolNode(tools)"
+ ]
},
{
"cell_type": "code",
@@ -345,7 +522,16 @@
"id": "4d5b75c6-73e0-4922-a765-a15be63f869e",
"metadata": {},
"outputs": [],
- "source": ["def _filter_out_routes(messages):\n ms = []\n for m in messages:\n if _is_tool_call(m):\n if m.name == \"general\":\n continue\n ms.append(m)\n return ms"]
+ "source": [
+ "def _filter_out_routes(messages):\n",
+ " ms = []\n",
+ " for m in messages:\n",
+ " if _is_tool_call(m):\n",
+ " if m.name == \"general\":\n",
+ " continue\n",
+ " ms.append(m)\n",
+ " return ms"
+ ]
},
{
"cell_type": "code",
@@ -353,7 +539,13 @@
"id": "fd4dbf98-dbb3-411a-bad6-2bb334072aaf",
"metadata": {},
"outputs": [],
- "source": ["from functools import partial\n\ngeneral_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\nmusic_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\ncustomer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"]
+ "source": [
+ "from functools import partial\n",
+ "\n",
+ "general_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\n",
+ "music_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\n",
+ "customer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"
+ ]
},
{
"cell_type": "code",
@@ -361,7 +553,33 @@
"id": "dcade924",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nfrom langgraph.graph import MessageGraph\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = MessageGraph()\nnodes = {\n \"general\": \"general\",\n \"music\": \"music\",\n END: END,\n \"tools\": \"tools\",\n \"customer\": \"customer\",\n}\n# Define a new graph\nworkflow = MessageGraph()\nworkflow.add_node(\"general\", general_node)\nworkflow.add_node(\"music\", music_node)\nworkflow.add_node(\"customer\", customer_node)\nworkflow.add_node(\"tools\", tool_node)\nworkflow.add_conditional_edges(\"general\", _route, nodes)\nworkflow.add_conditional_edges(\"tools\", _route, nodes)\nworkflow.add_conditional_edges(\"music\", _route, nodes)\nworkflow.add_conditional_edges(\"customer\", _route, nodes)\nworkflow.add_conditional_edges(START, _route, nodes)\ngraph = workflow.compile()"]
+ "source": [
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "\n",
+ "from langgraph.graph import MessageGraph\n",
+ "\n",
+ "memory = MemorySaver()\n",
+ "graph = MessageGraph()\n",
+ "nodes = {\n",
+ " \"general\": \"general\",\n",
+ " \"music\": \"music\",\n",
+ " END: END,\n",
+ " \"tools\": \"tools\",\n",
+ " \"customer\": \"customer\",\n",
+ "}\n",
+ "# Define a new graph\n",
+ "workflow = MessageGraph()\n",
+ "workflow.add_node(\"general\", general_node)\n",
+ "workflow.add_node(\"music\", music_node)\n",
+ "workflow.add_node(\"customer\", customer_node)\n",
+ "workflow.add_node(\"tools\", tool_node)\n",
+ "workflow.add_conditional_edges(\"general\", _route, nodes)\n",
+ "workflow.add_conditional_edges(\"tools\", _route, nodes)\n",
+ "workflow.add_conditional_edges(\"music\", _route, nodes)\n",
+ "workflow.add_conditional_edges(\"customer\", _route, nodes)\n",
+ "workflow.add_conditional_edges(START, _route, nodes)\n",
+ "graph = workflow.compile()"
+ ]
},
{
"cell_type": "code",
@@ -370,7 +588,7 @@
"metadata": {},
"outputs": [
{
- "name": "stdin",
+ "name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): what music do you have?\n"
@@ -395,7 +613,7 @@
]
},
{
- "name": "stdin",
+ "name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): how about shakira?\n"
@@ -446,7 +664,7 @@
]
},
{
- "name": "stdin",
+ "name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): hm cool\n"
@@ -483,7 +701,7 @@
]
},
{
- "name": "stdin",
+ "name": "stdout",
"output_type": "stream",
"text": [
"User (q/Q to quit): q\n"
@@ -497,7 +715,27 @@
]
}
],
- "source": ["import uuid\n\nfrom langchain_core.messages import HumanMessage\n\nfrom langgraph.graph.graph import START\n\nhistory = []\nwhile True:\n user = input(\"User (q/Q to quit): \")\n if user in {\"q\", \"Q\"}:\n print(\"AI: Byebye\")\n break\n history.append(HumanMessage(content=user))\n async for output in graph.astream(history):\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"]
+ "source": [
+ "import uuid\n",
+ "\n",
+ "from langchain_core.messages import HumanMessage\n",
+ "\n",
+ "from langgraph.graph.graph import START\n",
+ "\n",
+ "history = []\n",
+ "while True:\n",
+ " user = input(\"User (q/Q to quit): \")\n",
+ " if user in {\"q\", \"Q\"}:\n",
+ " print(\"AI: Byebye\")\n",
+ " break\n",
+ " history.append(HumanMessage(content=user))\n",
+ " async for output in graph.astream(history):\n",
+ " for key, value in output.items():\n",
+ " print(f\"Output from node '{key}':\")\n",
+ " print(\"---\")\n",
+ " print(value)\n",
+ " print(\"\\n---\\n\")"
+ ]
}
],
"metadata": {
diff --git a/examples/chatbots/information-gather-prompting.ipynb b/examples/chatbots/information-gather-prompting.ipynb
index ddc4fb309..079478dc7 100644
--- a/examples/chatbots/information-gather-prompting.ipynb
+++ b/examples/chatbots/information-gather-prompting.ipynb
@@ -176,10 +176,10 @@
"metadata": {},
"outputs": [],
"source": [
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import START, MessageGraph\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"workflow = MessageGraph()\n",
"workflow.add_node(\"info\", chain)\n",
"workflow.add_node(\"prompt\", prompt_gen_chain)\n",
diff --git a/examples/code_assistant/langgraph_code_assistant_mistral.ipynb b/examples/code_assistant/langgraph_code_assistant_mistral.ipynb
index fe947f11b..8d873d68c 100644
--- a/examples/code_assistant/langgraph_code_assistant_mistral.ipynb
+++ b/examples/code_assistant/langgraph_code_assistant_mistral.ipynb
@@ -154,7 +154,7 @@
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
"metadata": {},
"outputs": [],
- "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)"]
+ "source": ["from langgraph.checkpoint.memory import MemorySaver\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 = MemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
},
{
"cell_type": "code",
diff --git a/examples/customer-support/customer-support.ipynb b/examples/customer-support/customer-support.ipynb
index 3715ce4f0..cf451188f 100644
--- a/examples/customer-support/customer-support.ipynb
+++ b/examples/customer-support/customer-support.ipynb
@@ -32,7 +32,10 @@
"id": "afc570bf-e129-415b-8f2d-8bbce08131ab",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas"
+ ]
},
{
"cell_type": "code",
@@ -40,7 +43,24 @@
"id": "358e5666-b7c5-4e46-90a1-7ea273d86ee3",
"metadata": {},
"outputs": [],
- "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")\n_set_env(\"TAVILY_API_KEY\")\n\n# Recommended\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Customer Support Bot Tutorial\""]
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(var: str):\n",
+ " if not os.environ.get(var):\n",
+ " os.environ[var] = getpass.getpass(f\"{var}: \")\n",
+ "\n",
+ "\n",
+ "_set_env(\"ANTHROPIC_API_KEY\")\n",
+ "_set_env(\"TAVILY_API_KEY\")\n",
+ "\n",
+ "# Recommended\n",
+ "_set_env(\"LANGCHAIN_API_KEY\")\n",
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
+ "os.environ[\"LANGCHAIN_PROJECT\"] = \"Customer Support Bot Tutorial\""
+ ]
},
{
"cell_type": "markdown",
@@ -58,7 +78,68 @@
"id": "71638c2a-5038-439e-907a-de2bb548db34",
"metadata": {},
"outputs": [],
- "source": ["import os\nimport shutil\nimport sqlite3\n\nimport pandas as pd\nimport requests\n\ndb_url = \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/travel2.sqlite\"\nlocal_file = \"travel2.sqlite\"\n# The backup lets us restart for each tutorial section\nbackup_file = \"travel2.backup.sqlite\"\noverwrite = False\nif overwrite or not os.path.exists(local_file):\n response = requests.get(db_url)\n response.raise_for_status() # Ensure the request was successful\n with open(local_file, \"wb\") as f:\n f.write(response.content)\n # Backup - we will use this to \"reset\" our DB in each section\n shutil.copy(local_file, backup_file)\n# Convert the flights to present time for our tutorial\nconn = sqlite3.connect(local_file)\ncursor = conn.cursor()\n\ntables = pd.read_sql(\n \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n).name.tolist()\ntdf = {}\nfor t in tables:\n tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n\nexample_time = pd.to_datetime(\n tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n).max()\ncurrent_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\ntime_diff = current_time - example_time\n\ntdf[\"bookings\"][\"book_date\"] = (\n pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n + time_diff\n)\n\ndatetime_columns = [\n \"scheduled_departure\",\n \"scheduled_arrival\",\n \"actual_departure\",\n \"actual_arrival\",\n]\nfor column in datetime_columns:\n tdf[\"flights\"][column] = (\n pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n )\n\nfor table_name, df in tdf.items():\n df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\ndel df\ndel tdf\nconn.commit()\nconn.close()\n\ndb = local_file # We'll be using this local file as our DB in this tutorial"]
+ "source": [
+ "import os\n",
+ "import shutil\n",
+ "import sqlite3\n",
+ "\n",
+ "import pandas as pd\n",
+ "import requests\n",
+ "\n",
+ "db_url = \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/travel2.sqlite\"\n",
+ "local_file = \"travel2.sqlite\"\n",
+ "# The backup lets us restart for each tutorial section\n",
+ "backup_file = \"travel2.backup.sqlite\"\n",
+ "overwrite = False\n",
+ "if overwrite or not os.path.exists(local_file):\n",
+ " response = requests.get(db_url)\n",
+ " response.raise_for_status() # Ensure the request was successful\n",
+ " with open(local_file, \"wb\") as f:\n",
+ " f.write(response.content)\n",
+ " # Backup - we will use this to \"reset\" our DB in each section\n",
+ " shutil.copy(local_file, backup_file)\n",
+ "# Convert the flights to present time for our tutorial\n",
+ "conn = sqlite3.connect(local_file)\n",
+ "cursor = conn.cursor()\n",
+ "\n",
+ "tables = pd.read_sql(\n",
+ " \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
+ ").name.tolist()\n",
+ "tdf = {}\n",
+ "for t in tables:\n",
+ " tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
+ "\n",
+ "example_time = pd.to_datetime(\n",
+ " tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
+ ").max()\n",
+ "current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
+ "time_diff = current_time - example_time\n",
+ "\n",
+ "tdf[\"bookings\"][\"book_date\"] = (\n",
+ " pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
+ " + time_diff\n",
+ ")\n",
+ "\n",
+ "datetime_columns = [\n",
+ " \"scheduled_departure\",\n",
+ " \"scheduled_arrival\",\n",
+ " \"actual_departure\",\n",
+ " \"actual_arrival\",\n",
+ "]\n",
+ "for column in datetime_columns:\n",
+ " tdf[\"flights\"][column] = (\n",
+ " pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
+ " )\n",
+ "\n",
+ "for table_name, df in tdf.items():\n",
+ " df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
+ "del df\n",
+ "del tdf\n",
+ "conn.commit()\n",
+ "conn.close()\n",
+ "\n",
+ "db = local_file # We'll be using this local file as our DB in this tutorial"
+ ]
},
{
"cell_type": "markdown",
@@ -81,7 +162,59 @@
"id": "654e2f81",
"metadata": {},
"outputs": [],
- "source": ["import re\n\nimport numpy as np\nimport openai\nfrom langchain_core.tools import tool\n\nresponse = requests.get(\n \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/swiss_faq.md\"\n)\nresponse.raise_for_status()\nfaq_text = response.text\n\ndocs = [{\"page_content\": txt} for txt in re.split(r\"(?=\\n##)\", faq_text)]\n\n\nclass VectorStoreRetriever:\n def __init__(self, docs: list, vectors: list, oai_client):\n self._arr = np.array(vectors)\n self._docs = docs\n self._client = oai_client\n\n @classmethod\n def from_docs(cls, docs, oai_client):\n embeddings = oai_client.embeddings.create(\n model=\"text-embedding-3-small\", input=[doc[\"page_content\"] for doc in docs]\n )\n vectors = [emb.embedding for emb in embeddings.data]\n return cls(docs, vectors, oai_client)\n\n def query(self, query: str, k: int = 5) -> list[dict]:\n embed = self._client.embeddings.create(\n model=\"text-embedding-3-small\", input=[query]\n )\n # \"@\" is just a matrix multiplication in python\n scores = np.array(embed.data[0].embedding) @ self._arr.T\n top_k_idx = np.argpartition(scores, -k)[-k:]\n top_k_idx_sorted = top_k_idx[np.argsort(-scores[top_k_idx])]\n return [\n {**self._docs[idx], \"similarity\": scores[idx]} for idx in top_k_idx_sorted\n ]\n\n\nretriever = VectorStoreRetriever.from_docs(docs, openai.Client())\n\n\n@tool\ndef lookup_policy(query: str) -> str:\n \"\"\"Consult the company policies to check whether certain options are permitted.\n Use this before making any flight changes performing other 'write' events.\"\"\"\n docs = retriever.query(query, k=2)\n return \"\\n\\n\".join([doc[\"page_content\"] for doc in docs])"]
+ "source": [
+ "import re\n",
+ "\n",
+ "import numpy as np\n",
+ "import openai\n",
+ "from langchain_core.tools import tool\n",
+ "\n",
+ "response = requests.get(\n",
+ " \"https://storage.googleapis.com/benchmarks-artifacts/travel-db/swiss_faq.md\"\n",
+ ")\n",
+ "response.raise_for_status()\n",
+ "faq_text = response.text\n",
+ "\n",
+ "docs = [{\"page_content\": txt} for txt in re.split(r\"(?=\\n##)\", faq_text)]\n",
+ "\n",
+ "\n",
+ "class VectorStoreRetriever:\n",
+ " def __init__(self, docs: list, vectors: list, oai_client):\n",
+ " self._arr = np.array(vectors)\n",
+ " self._docs = docs\n",
+ " self._client = oai_client\n",
+ "\n",
+ " @classmethod\n",
+ " def from_docs(cls, docs, oai_client):\n",
+ " embeddings = oai_client.embeddings.create(\n",
+ " model=\"text-embedding-3-small\", input=[doc[\"page_content\"] for doc in docs]\n",
+ " )\n",
+ " vectors = [emb.embedding for emb in embeddings.data]\n",
+ " return cls(docs, vectors, oai_client)\n",
+ "\n",
+ " def query(self, query: str, k: int = 5) -> list[dict]:\n",
+ " embed = self._client.embeddings.create(\n",
+ " model=\"text-embedding-3-small\", input=[query]\n",
+ " )\n",
+ " # \"@\" is just a matrix multiplication in python\n",
+ " scores = np.array(embed.data[0].embedding) @ self._arr.T\n",
+ " top_k_idx = np.argpartition(scores, -k)[-k:]\n",
+ " top_k_idx_sorted = top_k_idx[np.argsort(-scores[top_k_idx])]\n",
+ " return [\n",
+ " {**self._docs[idx], \"similarity\": scores[idx]} for idx in top_k_idx_sorted\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "retriever = VectorStoreRetriever.from_docs(docs, openai.Client())\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def lookup_policy(query: str) -> str:\n",
+ " \"\"\"Consult the company policies to check whether certain options are permitted.\n",
+ " Use this before making any flight changes performing other 'write' events.\"\"\"\n",
+ " docs = retriever.query(query, k=2)\n",
+ " return \"\\n\\n\".join([doc[\"page_content\"] for doc in docs])"
+ ]
},
{
"cell_type": "markdown",
@@ -101,7 +234,205 @@
"id": "043b4341",
"metadata": {},
"outputs": [],
- "source": ["import sqlite3\nfrom datetime import date, datetime\nfrom typing import Optional\n\nimport pytz\nfrom langchain_core.runnables import ensure_config\n\n\n@tool\ndef fetch_user_flight_information() -> list[dict]:\n \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n\n Returns:\n A list of dictionaries where each dictionary contains the ticket details,\n associated flight details, and the seat assignments for each ticket belonging to the user.\n \"\"\"\n config = ensure_config() # Fetch from the context\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n if not passenger_id:\n raise ValueError(\"No passenger ID configured.\")\n\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"\"\"\n SELECT \n t.ticket_no, t.book_ref,\n f.flight_id, f.flight_no, f.departure_airport, f.arrival_airport, f.scheduled_departure, f.scheduled_arrival,\n bp.seat_no, tf.fare_conditions\n FROM \n tickets t\n JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no\n JOIN flights f ON tf.flight_id = f.flight_id\n JOIN boarding_passes bp ON bp.ticket_no = t.ticket_no AND bp.flight_id = f.flight_id\n WHERE \n t.passenger_id = ?\n \"\"\"\n cursor.execute(query, (passenger_id,))\n rows = cursor.fetchall()\n column_names = [column[0] for column in cursor.description]\n results = [dict(zip(column_names, row)) for row in rows]\n\n cursor.close()\n conn.close()\n\n return results\n\n\n@tool\ndef search_flights(\n departure_airport: Optional[str] = None,\n arrival_airport: Optional[str] = None,\n start_time: Optional[date | datetime] = None,\n end_time: Optional[date | datetime] = None,\n limit: int = 20,\n) -> list[dict]:\n \"\"\"Search for flights based on departure airport, arrival airport, and departure time range.\"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM flights WHERE 1 = 1\"\n params = []\n\n if departure_airport:\n query += \" AND departure_airport = ?\"\n params.append(departure_airport)\n\n if arrival_airport:\n query += \" AND arrival_airport = ?\"\n params.append(arrival_airport)\n\n if start_time:\n query += \" AND scheduled_departure >= ?\"\n params.append(start_time)\n\n if end_time:\n query += \" AND scheduled_departure <= ?\"\n params.append(end_time)\n query += \" LIMIT ?\"\n params.append(limit)\n cursor.execute(query, params)\n rows = cursor.fetchall()\n column_names = [column[0] for column in cursor.description]\n results = [dict(zip(column_names, row)) for row in rows]\n\n cursor.close()\n conn.close()\n\n return results\n\n\n@tool\ndef update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n config = ensure_config()\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n if not passenger_id:\n raise ValueError(\"No passenger ID configured.\")\n\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"SELECT departure_airport, arrival_airport, scheduled_departure FROM flights WHERE flight_id = ?\",\n (new_flight_id,),\n )\n new_flight = cursor.fetchone()\n if not new_flight:\n cursor.close()\n conn.close()\n return \"Invalid new flight ID provided.\"\n column_names = [column[0] for column in cursor.description]\n new_flight_dict = dict(zip(column_names, new_flight))\n timezone = pytz.timezone(\"Etc/GMT-3\")\n current_time = datetime.now(tz=timezone)\n departure_time = datetime.strptime(\n new_flight_dict[\"scheduled_departure\"], \"%Y-%m-%d %H:%M:%S.%f%z\"\n )\n time_until = (departure_time - current_time).total_seconds()\n if time_until < (3 * 3600):\n return f\"Not permitted to reschedule to a flight that is less than 3 hours from the current time. Selected flight is at {departure_time}.\"\n\n cursor.execute(\n \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n )\n current_flight = cursor.fetchone()\n if not current_flight:\n cursor.close()\n conn.close()\n return \"No existing ticket found for the given ticket number.\"\n\n # Check the signed-in user actually has this ticket\n cursor.execute(\n \"SELECT * FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n (ticket_no, passenger_id),\n )\n current_ticket = cursor.fetchone()\n if not current_ticket:\n cursor.close()\n conn.close()\n return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n\n # In a real application, you'd likely add additional checks here to enforce business logic,\n # like \"does the new departure airport match the current ticket\", etc.\n # While it's best to try to be *proactive* in 'type-hinting' policies to the LLM\n # it's inevitably going to get things wrong, so you **also** need to ensure your\n # API enforces valid behavior\n cursor.execute(\n \"UPDATE ticket_flights SET flight_id = ? WHERE ticket_no = ?\",\n (new_flight_id, ticket_no),\n )\n conn.commit()\n\n cursor.close()\n conn.close()\n return \"Ticket successfully updated to new flight.\"\n\n\n@tool\ndef cancel_ticket(ticket_no: str) -> str:\n \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n config = ensure_config()\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n if not passenger_id:\n raise ValueError(\"No passenger ID configured.\")\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n )\n existing_ticket = cursor.fetchone()\n if not existing_ticket:\n cursor.close()\n conn.close()\n return \"No existing ticket found for the given ticket number.\"\n\n # Check the signed-in user actually has this ticket\n cursor.execute(\n \"SELECT flight_id FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n (ticket_no, passenger_id),\n )\n current_ticket = cursor.fetchone()\n if not current_ticket:\n cursor.close()\n conn.close()\n return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n\n cursor.execute(\"DELETE FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,))\n conn.commit()\n\n cursor.close()\n conn.close()\n return \"Ticket successfully cancelled.\""]
+ "source": [
+ "import sqlite3\n",
+ "from datetime import date, datetime\n",
+ "from typing import Optional\n",
+ "\n",
+ "import pytz\n",
+ "from langchain_core.runnables import ensure_config\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def fetch_user_flight_information() -> list[dict]:\n",
+ " \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n",
+ "\n",
+ " Returns:\n",
+ " A list of dictionaries where each dictionary contains the ticket details,\n",
+ " associated flight details, and the seat assignments for each ticket belonging to the user.\n",
+ " \"\"\"\n",
+ " config = ensure_config() # Fetch from the context\n",
+ " configuration = config.get(\"configurable\", {})\n",
+ " passenger_id = configuration.get(\"passenger_id\", None)\n",
+ " if not passenger_id:\n",
+ " raise ValueError(\"No passenger ID configured.\")\n",
+ "\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " query = \"\"\"\n",
+ " SELECT \n",
+ " t.ticket_no, t.book_ref,\n",
+ " f.flight_id, f.flight_no, f.departure_airport, f.arrival_airport, f.scheduled_departure, f.scheduled_arrival,\n",
+ " bp.seat_no, tf.fare_conditions\n",
+ " FROM \n",
+ " tickets t\n",
+ " JOIN ticket_flights tf ON t.ticket_no = tf.ticket_no\n",
+ " JOIN flights f ON tf.flight_id = f.flight_id\n",
+ " JOIN boarding_passes bp ON bp.ticket_no = t.ticket_no AND bp.flight_id = f.flight_id\n",
+ " WHERE \n",
+ " t.passenger_id = ?\n",
+ " \"\"\"\n",
+ " cursor.execute(query, (passenger_id,))\n",
+ " rows = cursor.fetchall()\n",
+ " column_names = [column[0] for column in cursor.description]\n",
+ " results = [dict(zip(column_names, row)) for row in rows]\n",
+ "\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ "\n",
+ " return results\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def search_flights(\n",
+ " departure_airport: Optional[str] = None,\n",
+ " arrival_airport: Optional[str] = None,\n",
+ " start_time: Optional[date | datetime] = None,\n",
+ " end_time: Optional[date | datetime] = None,\n",
+ " limit: int = 20,\n",
+ ") -> list[dict]:\n",
+ " \"\"\"Search for flights based on departure airport, arrival airport, and departure time range.\"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " query = \"SELECT * FROM flights WHERE 1 = 1\"\n",
+ " params = []\n",
+ "\n",
+ " if departure_airport:\n",
+ " query += \" AND departure_airport = ?\"\n",
+ " params.append(departure_airport)\n",
+ "\n",
+ " if arrival_airport:\n",
+ " query += \" AND arrival_airport = ?\"\n",
+ " params.append(arrival_airport)\n",
+ "\n",
+ " if start_time:\n",
+ " query += \" AND scheduled_departure >= ?\"\n",
+ " params.append(start_time)\n",
+ "\n",
+ " if end_time:\n",
+ " query += \" AND scheduled_departure <= ?\"\n",
+ " params.append(end_time)\n",
+ " query += \" LIMIT ?\"\n",
+ " params.append(limit)\n",
+ " cursor.execute(query, params)\n",
+ " rows = cursor.fetchall()\n",
+ " column_names = [column[0] for column in cursor.description]\n",
+ " results = [dict(zip(column_names, row)) for row in rows]\n",
+ "\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ "\n",
+ " return results\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n",
+ " \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n",
+ " config = ensure_config()\n",
+ " configuration = config.get(\"configurable\", {})\n",
+ " passenger_id = configuration.get(\"passenger_id\", None)\n",
+ " if not passenger_id:\n",
+ " raise ValueError(\"No passenger ID configured.\")\n",
+ "\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\n",
+ " \"SELECT departure_airport, arrival_airport, scheduled_departure FROM flights WHERE flight_id = ?\",\n",
+ " (new_flight_id,),\n",
+ " )\n",
+ " new_flight = cursor.fetchone()\n",
+ " if not new_flight:\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return \"Invalid new flight ID provided.\"\n",
+ " column_names = [column[0] for column in cursor.description]\n",
+ " new_flight_dict = dict(zip(column_names, new_flight))\n",
+ " timezone = pytz.timezone(\"Etc/GMT-3\")\n",
+ " current_time = datetime.now(tz=timezone)\n",
+ " departure_time = datetime.strptime(\n",
+ " new_flight_dict[\"scheduled_departure\"], \"%Y-%m-%d %H:%M:%S.%f%z\"\n",
+ " )\n",
+ " time_until = (departure_time - current_time).total_seconds()\n",
+ " if time_until < (3 * 3600):\n",
+ " return f\"Not permitted to reschedule to a flight that is less than 3 hours from the current time. Selected flight is at {departure_time}.\"\n",
+ "\n",
+ " cursor.execute(\n",
+ " \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n",
+ " )\n",
+ " current_flight = cursor.fetchone()\n",
+ " if not current_flight:\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return \"No existing ticket found for the given ticket number.\"\n",
+ "\n",
+ " # Check the signed-in user actually has this ticket\n",
+ " cursor.execute(\n",
+ " \"SELECT * FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n",
+ " (ticket_no, passenger_id),\n",
+ " )\n",
+ " current_ticket = cursor.fetchone()\n",
+ " if not current_ticket:\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n",
+ "\n",
+ " # In a real application, you'd likely add additional checks here to enforce business logic,\n",
+ " # like \"does the new departure airport match the current ticket\", etc.\n",
+ " # While it's best to try to be *proactive* in 'type-hinting' policies to the LLM\n",
+ " # it's inevitably going to get things wrong, so you **also** need to ensure your\n",
+ " # API enforces valid behavior\n",
+ " cursor.execute(\n",
+ " \"UPDATE ticket_flights SET flight_id = ? WHERE ticket_no = ?\",\n",
+ " (new_flight_id, ticket_no),\n",
+ " )\n",
+ " conn.commit()\n",
+ "\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return \"Ticket successfully updated to new flight.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def cancel_ticket(ticket_no: str) -> str:\n",
+ " \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n",
+ " config = ensure_config()\n",
+ " configuration = config.get(\"configurable\", {})\n",
+ " passenger_id = configuration.get(\"passenger_id\", None)\n",
+ " if not passenger_id:\n",
+ " raise ValueError(\"No passenger ID configured.\")\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\n",
+ " \"SELECT flight_id FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,)\n",
+ " )\n",
+ " existing_ticket = cursor.fetchone()\n",
+ " if not existing_ticket:\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return \"No existing ticket found for the given ticket number.\"\n",
+ "\n",
+ " # Check the signed-in user actually has this ticket\n",
+ " cursor.execute(\n",
+ " \"SELECT flight_id FROM tickets WHERE ticket_no = ? AND passenger_id = ?\",\n",
+ " (ticket_no, passenger_id),\n",
+ " )\n",
+ " current_ticket = cursor.fetchone()\n",
+ " if not current_ticket:\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return f\"Current signed-in passenger with ID {passenger_id} not the owner of ticket {ticket_no}\"\n",
+ "\n",
+ " cursor.execute(\"DELETE FROM ticket_flights WHERE ticket_no = ?\", (ticket_no,))\n",
+ " conn.commit()\n",
+ "\n",
+ " cursor.close()\n",
+ " conn.close()\n",
+ " return \"Ticket successfully cancelled.\""
+ ]
},
{
"cell_type": "markdown",
@@ -119,7 +450,145 @@
"id": "f3edabaf-7a23-4f9f-9c57-97b799bc21df",
"metadata": {},
"outputs": [],
- "source": ["from datetime import date, datetime\nfrom typing import Optional, Union\n\n\n@tool\ndef search_car_rentals(\n location: Optional[str] = None,\n name: Optional[str] = None,\n price_tier: Optional[str] = None,\n start_date: Optional[Union[datetime, date]] = None,\n end_date: Optional[Union[datetime, date]] = None,\n) -> list[dict]:\n \"\"\"\n Search for car rentals based on location, name, price tier, start date, and end date.\n\n Args:\n location (Optional[str]): The location of the car rental. Defaults to None.\n name (Optional[str]): The name of the car rental company. Defaults to None.\n price_tier (Optional[str]): The price tier of the car rental. Defaults to None.\n start_date (Optional[Union[datetime, date]]): The start date of the car rental. Defaults to None.\n end_date (Optional[Union[datetime, date]]): The end date of the car rental. Defaults to None.\n\n Returns:\n list[dict]: A list of car rental dictionaries matching the search criteria.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM car_rentals WHERE 1=1\"\n params = []\n\n if location:\n query += \" AND location LIKE ?\"\n params.append(f\"%{location}%\")\n if name:\n query += \" AND name LIKE ?\"\n params.append(f\"%{name}%\")\n # For our tutorial, we will let you match on any dates and price tier.\n # (since our toy dataset doesn't have much data)\n cursor.execute(query, params)\n results = cursor.fetchall()\n\n conn.close()\n\n return [\n dict(zip([column[0] for column in cursor.description], row)) for row in results\n ]\n\n\n@tool\ndef book_car_rental(rental_id: int) -> str:\n \"\"\"\n Book a car rental by its ID.\n\n Args:\n rental_id (int): The ID of the car rental to book.\n\n Returns:\n str: A message indicating whether the car rental was successfully booked or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE car_rentals SET booked = 1 WHERE id = ?\", (rental_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Car rental {rental_id} successfully booked.\"\n else:\n conn.close()\n return f\"No car rental found with ID {rental_id}.\"\n\n\n@tool\ndef update_car_rental(\n rental_id: int,\n start_date: Optional[Union[datetime, date]] = None,\n end_date: Optional[Union[datetime, date]] = None,\n) -> str:\n \"\"\"\n Update a car rental's start and end dates by its ID.\n\n Args:\n rental_id (int): The ID of the car rental to update.\n start_date (Optional[Union[datetime, date]]): The new start date of the car rental. Defaults to None.\n end_date (Optional[Union[datetime, date]]): The new end date of the car rental. Defaults to None.\n\n Returns:\n str: A message indicating whether the car rental was successfully updated or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n if start_date:\n cursor.execute(\n \"UPDATE car_rentals SET start_date = ? WHERE id = ?\",\n (start_date, rental_id),\n )\n if end_date:\n cursor.execute(\n \"UPDATE car_rentals SET end_date = ? WHERE id = ?\", (end_date, rental_id)\n )\n\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Car rental {rental_id} successfully updated.\"\n else:\n conn.close()\n return f\"No car rental found with ID {rental_id}.\"\n\n\n@tool\ndef cancel_car_rental(rental_id: int) -> str:\n \"\"\"\n Cancel a car rental by its ID.\n\n Args:\n rental_id (int): The ID of the car rental to cancel.\n\n Returns:\n str: A message indicating whether the car rental was successfully cancelled or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE car_rentals SET booked = 0 WHERE id = ?\", (rental_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Car rental {rental_id} successfully cancelled.\"\n else:\n conn.close()\n return f\"No car rental found with ID {rental_id}.\""]
+ "source": [
+ "from datetime import date, datetime\n",
+ "from typing import Optional, Union\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def search_car_rentals(\n",
+ " location: Optional[str] = None,\n",
+ " name: Optional[str] = None,\n",
+ " price_tier: Optional[str] = None,\n",
+ " start_date: Optional[Union[datetime, date]] = None,\n",
+ " end_date: Optional[Union[datetime, date]] = None,\n",
+ ") -> list[dict]:\n",
+ " \"\"\"\n",
+ " Search for car rentals based on location, name, price tier, start date, and end date.\n",
+ "\n",
+ " Args:\n",
+ " location (Optional[str]): The location of the car rental. Defaults to None.\n",
+ " name (Optional[str]): The name of the car rental company. Defaults to None.\n",
+ " price_tier (Optional[str]): The price tier of the car rental. Defaults to None.\n",
+ " start_date (Optional[Union[datetime, date]]): The start date of the car rental. Defaults to None.\n",
+ " end_date (Optional[Union[datetime, date]]): The end date of the car rental. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " list[dict]: A list of car rental dictionaries matching the search criteria.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " query = \"SELECT * FROM car_rentals WHERE 1=1\"\n",
+ " params = []\n",
+ "\n",
+ " if location:\n",
+ " query += \" AND location LIKE ?\"\n",
+ " params.append(f\"%{location}%\")\n",
+ " if name:\n",
+ " query += \" AND name LIKE ?\"\n",
+ " params.append(f\"%{name}%\")\n",
+ " # For our tutorial, we will let you match on any dates and price tier.\n",
+ " # (since our toy dataset doesn't have much data)\n",
+ " cursor.execute(query, params)\n",
+ " results = cursor.fetchall()\n",
+ "\n",
+ " conn.close()\n",
+ "\n",
+ " return [\n",
+ " dict(zip([column[0] for column in cursor.description], row)) for row in results\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def book_car_rental(rental_id: int) -> str:\n",
+ " \"\"\"\n",
+ " Book a car rental by its ID.\n",
+ "\n",
+ " Args:\n",
+ " rental_id (int): The ID of the car rental to book.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the car rental was successfully booked or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\"UPDATE car_rentals SET booked = 1 WHERE id = ?\", (rental_id,))\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Car rental {rental_id} successfully booked.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No car rental found with ID {rental_id}.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def update_car_rental(\n",
+ " rental_id: int,\n",
+ " start_date: Optional[Union[datetime, date]] = None,\n",
+ " end_date: Optional[Union[datetime, date]] = None,\n",
+ ") -> str:\n",
+ " \"\"\"\n",
+ " Update a car rental's start and end dates by its ID.\n",
+ "\n",
+ " Args:\n",
+ " rental_id (int): The ID of the car rental to update.\n",
+ " start_date (Optional[Union[datetime, date]]): The new start date of the car rental. Defaults to None.\n",
+ " end_date (Optional[Union[datetime, date]]): The new end date of the car rental. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the car rental was successfully updated or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " if start_date:\n",
+ " cursor.execute(\n",
+ " \"UPDATE car_rentals SET start_date = ? WHERE id = ?\",\n",
+ " (start_date, rental_id),\n",
+ " )\n",
+ " if end_date:\n",
+ " cursor.execute(\n",
+ " \"UPDATE car_rentals SET end_date = ? WHERE id = ?\", (end_date, rental_id)\n",
+ " )\n",
+ "\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Car rental {rental_id} successfully updated.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No car rental found with ID {rental_id}.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def cancel_car_rental(rental_id: int) -> str:\n",
+ " \"\"\"\n",
+ " Cancel a car rental by its ID.\n",
+ "\n",
+ " Args:\n",
+ " rental_id (int): The ID of the car rental to cancel.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the car rental was successfully cancelled or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\"UPDATE car_rentals SET booked = 0 WHERE id = ?\", (rental_id,))\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Car rental {rental_id} successfully cancelled.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No car rental found with ID {rental_id}.\""
+ ]
},
{
"cell_type": "markdown",
@@ -137,7 +606,140 @@
"id": "a8e4ab3c-0086-4257-855b-97cc4037513f",
"metadata": {},
"outputs": [],
- "source": ["@tool\ndef search_hotels(\n location: Optional[str] = None,\n name: Optional[str] = None,\n price_tier: Optional[str] = None,\n checkin_date: Optional[Union[datetime, date]] = None,\n checkout_date: Optional[Union[datetime, date]] = None,\n) -> list[dict]:\n \"\"\"\n Search for hotels based on location, name, price tier, check-in date, and check-out date.\n\n Args:\n location (Optional[str]): The location of the hotel. Defaults to None.\n name (Optional[str]): The name of the hotel. Defaults to None.\n price_tier (Optional[str]): The price tier of the hotel. Defaults to None. Examples: Midscale, Upper Midscale, Upscale, Luxury\n checkin_date (Optional[Union[datetime, date]]): The check-in date of the hotel. Defaults to None.\n checkout_date (Optional[Union[datetime, date]]): The check-out date of the hotel. Defaults to None.\n\n Returns:\n list[dict]: A list of hotel dictionaries matching the search criteria.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM hotels WHERE 1=1\"\n params = []\n\n if location:\n query += \" AND location LIKE ?\"\n params.append(f\"%{location}%\")\n if name:\n query += \" AND name LIKE ?\"\n params.append(f\"%{name}%\")\n # For the sake of this tutorial, we will let you match on any dates and price tier.\n cursor.execute(query, params)\n results = cursor.fetchall()\n\n conn.close()\n\n return [\n dict(zip([column[0] for column in cursor.description], row)) for row in results\n ]\n\n\n@tool\ndef book_hotel(hotel_id: int) -> str:\n \"\"\"\n Book a hotel by its ID.\n\n Args:\n hotel_id (int): The ID of the hotel to book.\n\n Returns:\n str: A message indicating whether the hotel was successfully booked or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE hotels SET booked = 1 WHERE id = ?\", (hotel_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Hotel {hotel_id} successfully booked.\"\n else:\n conn.close()\n return f\"No hotel found with ID {hotel_id}.\"\n\n\n@tool\ndef update_hotel(\n hotel_id: int,\n checkin_date: Optional[Union[datetime, date]] = None,\n checkout_date: Optional[Union[datetime, date]] = None,\n) -> str:\n \"\"\"\n Update a hotel's check-in and check-out dates by its ID.\n\n Args:\n hotel_id (int): The ID of the hotel to update.\n checkin_date (Optional[Union[datetime, date]]): The new check-in date of the hotel. Defaults to None.\n checkout_date (Optional[Union[datetime, date]]): The new check-out date of the hotel. Defaults to None.\n\n Returns:\n str: A message indicating whether the hotel was successfully updated or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n if checkin_date:\n cursor.execute(\n \"UPDATE hotels SET checkin_date = ? WHERE id = ?\", (checkin_date, hotel_id)\n )\n if checkout_date:\n cursor.execute(\n \"UPDATE hotels SET checkout_date = ? WHERE id = ?\",\n (checkout_date, hotel_id),\n )\n\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Hotel {hotel_id} successfully updated.\"\n else:\n conn.close()\n return f\"No hotel found with ID {hotel_id}.\"\n\n\n@tool\ndef cancel_hotel(hotel_id: int) -> str:\n \"\"\"\n Cancel a hotel by its ID.\n\n Args:\n hotel_id (int): The ID of the hotel to cancel.\n\n Returns:\n str: A message indicating whether the hotel was successfully cancelled or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\"UPDATE hotels SET booked = 0 WHERE id = ?\", (hotel_id,))\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Hotel {hotel_id} successfully cancelled.\"\n else:\n conn.close()\n return f\"No hotel found with ID {hotel_id}.\""]
+ "source": [
+ "@tool\n",
+ "def search_hotels(\n",
+ " location: Optional[str] = None,\n",
+ " name: Optional[str] = None,\n",
+ " price_tier: Optional[str] = None,\n",
+ " checkin_date: Optional[Union[datetime, date]] = None,\n",
+ " checkout_date: Optional[Union[datetime, date]] = None,\n",
+ ") -> list[dict]:\n",
+ " \"\"\"\n",
+ " Search for hotels based on location, name, price tier, check-in date, and check-out date.\n",
+ "\n",
+ " Args:\n",
+ " location (Optional[str]): The location of the hotel. Defaults to None.\n",
+ " name (Optional[str]): The name of the hotel. Defaults to None.\n",
+ " price_tier (Optional[str]): The price tier of the hotel. Defaults to None. Examples: Midscale, Upper Midscale, Upscale, Luxury\n",
+ " checkin_date (Optional[Union[datetime, date]]): The check-in date of the hotel. Defaults to None.\n",
+ " checkout_date (Optional[Union[datetime, date]]): The check-out date of the hotel. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " list[dict]: A list of hotel dictionaries matching the search criteria.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " query = \"SELECT * FROM hotels WHERE 1=1\"\n",
+ " params = []\n",
+ "\n",
+ " if location:\n",
+ " query += \" AND location LIKE ?\"\n",
+ " params.append(f\"%{location}%\")\n",
+ " if name:\n",
+ " query += \" AND name LIKE ?\"\n",
+ " params.append(f\"%{name}%\")\n",
+ " # For the sake of this tutorial, we will let you match on any dates and price tier.\n",
+ " cursor.execute(query, params)\n",
+ " results = cursor.fetchall()\n",
+ "\n",
+ " conn.close()\n",
+ "\n",
+ " return [\n",
+ " dict(zip([column[0] for column in cursor.description], row)) for row in results\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def book_hotel(hotel_id: int) -> str:\n",
+ " \"\"\"\n",
+ " Book a hotel by its ID.\n",
+ "\n",
+ " Args:\n",
+ " hotel_id (int): The ID of the hotel to book.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the hotel was successfully booked or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\"UPDATE hotels SET booked = 1 WHERE id = ?\", (hotel_id,))\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Hotel {hotel_id} successfully booked.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No hotel found with ID {hotel_id}.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def update_hotel(\n",
+ " hotel_id: int,\n",
+ " checkin_date: Optional[Union[datetime, date]] = None,\n",
+ " checkout_date: Optional[Union[datetime, date]] = None,\n",
+ ") -> str:\n",
+ " \"\"\"\n",
+ " Update a hotel's check-in and check-out dates by its ID.\n",
+ "\n",
+ " Args:\n",
+ " hotel_id (int): The ID of the hotel to update.\n",
+ " checkin_date (Optional[Union[datetime, date]]): The new check-in date of the hotel. Defaults to None.\n",
+ " checkout_date (Optional[Union[datetime, date]]): The new check-out date of the hotel. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the hotel was successfully updated or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " if checkin_date:\n",
+ " cursor.execute(\n",
+ " \"UPDATE hotels SET checkin_date = ? WHERE id = ?\", (checkin_date, hotel_id)\n",
+ " )\n",
+ " if checkout_date:\n",
+ " cursor.execute(\n",
+ " \"UPDATE hotels SET checkout_date = ? WHERE id = ?\",\n",
+ " (checkout_date, hotel_id),\n",
+ " )\n",
+ "\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Hotel {hotel_id} successfully updated.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No hotel found with ID {hotel_id}.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def cancel_hotel(hotel_id: int) -> str:\n",
+ " \"\"\"\n",
+ " Cancel a hotel by its ID.\n",
+ "\n",
+ " Args:\n",
+ " hotel_id (int): The ID of the hotel to cancel.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the hotel was successfully cancelled or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\"UPDATE hotels SET booked = 0 WHERE id = ?\", (hotel_id,))\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Hotel {hotel_id} successfully cancelled.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No hotel found with ID {hotel_id}.\""
+ ]
},
{
"cell_type": "markdown",
@@ -155,7 +757,134 @@
"id": "2260eccb-8ae2-4a41-a1ba-f78ee3df3010",
"metadata": {},
"outputs": [],
- "source": ["@tool\ndef search_trip_recommendations(\n location: Optional[str] = None,\n name: Optional[str] = None,\n keywords: Optional[str] = None,\n) -> list[dict]:\n \"\"\"\n Search for trip recommendations based on location, name, and keywords.\n\n Args:\n location (Optional[str]): The location of the trip recommendation. Defaults to None.\n name (Optional[str]): The name of the trip recommendation. Defaults to None.\n keywords (Optional[str]): The keywords associated with the trip recommendation. Defaults to None.\n\n Returns:\n list[dict]: A list of trip recommendation dictionaries matching the search criteria.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n query = \"SELECT * FROM trip_recommendations WHERE 1=1\"\n params = []\n\n if location:\n query += \" AND location LIKE ?\"\n params.append(f\"%{location}%\")\n if name:\n query += \" AND name LIKE ?\"\n params.append(f\"%{name}%\")\n if keywords:\n keyword_list = keywords.split(\",\")\n keyword_conditions = \" OR \".join([\"keywords LIKE ?\" for _ in keyword_list])\n query += f\" AND ({keyword_conditions})\"\n params.extend([f\"%{keyword.strip()}%\" for keyword in keyword_list])\n\n cursor.execute(query, params)\n results = cursor.fetchall()\n\n conn.close()\n\n return [\n dict(zip([column[0] for column in cursor.description], row)) for row in results\n ]\n\n\n@tool\ndef book_excursion(recommendation_id: int) -> str:\n \"\"\"\n Book a excursion by its recommendation ID.\n\n Args:\n recommendation_id (int): The ID of the trip recommendation to book.\n\n Returns:\n str: A message indicating whether the trip recommendation was successfully booked or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"UPDATE trip_recommendations SET booked = 1 WHERE id = ?\", (recommendation_id,)\n )\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Trip recommendation {recommendation_id} successfully booked.\"\n else:\n conn.close()\n return f\"No trip recommendation found with ID {recommendation_id}.\"\n\n\n@tool\ndef update_excursion(recommendation_id: int, details: str) -> str:\n \"\"\"\n Update a trip recommendation's details by its ID.\n\n Args:\n recommendation_id (int): The ID of the trip recommendation to update.\n details (str): The new details of the trip recommendation.\n\n Returns:\n str: A message indicating whether the trip recommendation was successfully updated or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"UPDATE trip_recommendations SET details = ? WHERE id = ?\",\n (details, recommendation_id),\n )\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Trip recommendation {recommendation_id} successfully updated.\"\n else:\n conn.close()\n return f\"No trip recommendation found with ID {recommendation_id}.\"\n\n\n@tool\ndef cancel_excursion(recommendation_id: int) -> str:\n \"\"\"\n Cancel a trip recommendation by its ID.\n\n Args:\n recommendation_id (int): The ID of the trip recommendation to cancel.\n\n Returns:\n str: A message indicating whether the trip recommendation was successfully cancelled or not.\n \"\"\"\n conn = sqlite3.connect(db)\n cursor = conn.cursor()\n\n cursor.execute(\n \"UPDATE trip_recommendations SET booked = 0 WHERE id = ?\", (recommendation_id,)\n )\n conn.commit()\n\n if cursor.rowcount > 0:\n conn.close()\n return f\"Trip recommendation {recommendation_id} successfully cancelled.\"\n else:\n conn.close()\n return f\"No trip recommendation found with ID {recommendation_id}.\""]
+ "source": [
+ "@tool\n",
+ "def search_trip_recommendations(\n",
+ " location: Optional[str] = None,\n",
+ " name: Optional[str] = None,\n",
+ " keywords: Optional[str] = None,\n",
+ ") -> list[dict]:\n",
+ " \"\"\"\n",
+ " Search for trip recommendations based on location, name, and keywords.\n",
+ "\n",
+ " Args:\n",
+ " location (Optional[str]): The location of the trip recommendation. Defaults to None.\n",
+ " name (Optional[str]): The name of the trip recommendation. Defaults to None.\n",
+ " keywords (Optional[str]): The keywords associated with the trip recommendation. Defaults to None.\n",
+ "\n",
+ " Returns:\n",
+ " list[dict]: A list of trip recommendation dictionaries matching the search criteria.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " query = \"SELECT * FROM trip_recommendations WHERE 1=1\"\n",
+ " params = []\n",
+ "\n",
+ " if location:\n",
+ " query += \" AND location LIKE ?\"\n",
+ " params.append(f\"%{location}%\")\n",
+ " if name:\n",
+ " query += \" AND name LIKE ?\"\n",
+ " params.append(f\"%{name}%\")\n",
+ " if keywords:\n",
+ " keyword_list = keywords.split(\",\")\n",
+ " keyword_conditions = \" OR \".join([\"keywords LIKE ?\" for _ in keyword_list])\n",
+ " query += f\" AND ({keyword_conditions})\"\n",
+ " params.extend([f\"%{keyword.strip()}%\" for keyword in keyword_list])\n",
+ "\n",
+ " cursor.execute(query, params)\n",
+ " results = cursor.fetchall()\n",
+ "\n",
+ " conn.close()\n",
+ "\n",
+ " return [\n",
+ " dict(zip([column[0] for column in cursor.description], row)) for row in results\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def book_excursion(recommendation_id: int) -> str:\n",
+ " \"\"\"\n",
+ " Book a excursion by its recommendation ID.\n",
+ "\n",
+ " Args:\n",
+ " recommendation_id (int): The ID of the trip recommendation to book.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the trip recommendation was successfully booked or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\n",
+ " \"UPDATE trip_recommendations SET booked = 1 WHERE id = ?\", (recommendation_id,)\n",
+ " )\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Trip recommendation {recommendation_id} successfully booked.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No trip recommendation found with ID {recommendation_id}.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def update_excursion(recommendation_id: int, details: str) -> str:\n",
+ " \"\"\"\n",
+ " Update a trip recommendation's details by its ID.\n",
+ "\n",
+ " Args:\n",
+ " recommendation_id (int): The ID of the trip recommendation to update.\n",
+ " details (str): The new details of the trip recommendation.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the trip recommendation was successfully updated or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\n",
+ " \"UPDATE trip_recommendations SET details = ? WHERE id = ?\",\n",
+ " (details, recommendation_id),\n",
+ " )\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Trip recommendation {recommendation_id} successfully updated.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No trip recommendation found with ID {recommendation_id}.\"\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def cancel_excursion(recommendation_id: int) -> str:\n",
+ " \"\"\"\n",
+ " Cancel a trip recommendation by its ID.\n",
+ "\n",
+ " Args:\n",
+ " recommendation_id (int): The ID of the trip recommendation to cancel.\n",
+ "\n",
+ " Returns:\n",
+ " str: A message indicating whether the trip recommendation was successfully cancelled or not.\n",
+ " \"\"\"\n",
+ " conn = sqlite3.connect(db)\n",
+ " cursor = conn.cursor()\n",
+ "\n",
+ " cursor.execute(\n",
+ " \"UPDATE trip_recommendations SET booked = 0 WHERE id = ?\", (recommendation_id,)\n",
+ " )\n",
+ " conn.commit()\n",
+ "\n",
+ " if cursor.rowcount > 0:\n",
+ " conn.close()\n",
+ " return f\"Trip recommendation {recommendation_id} successfully cancelled.\"\n",
+ " else:\n",
+ " conn.close()\n",
+ " return f\"No trip recommendation found with ID {recommendation_id}.\""
+ ]
},
{
"cell_type": "markdown",
@@ -173,7 +902,48 @@
"id": "663f001e",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import ToolMessage\nfrom langchain_core.runnables import RunnableLambda\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef handle_tool_error(state) -> dict:\n error = state.get(\"error\")\n tool_calls = state[\"messages\"][-1].tool_calls\n return {\n \"messages\": [\n ToolMessage(\n content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n tool_call_id=tc[\"id\"],\n )\n for tc in tool_calls\n ]\n }\n\n\ndef create_tool_node_with_fallback(tools: list) -> dict:\n return ToolNode(tools).with_fallbacks(\n [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n )\n\n\ndef _print_event(event: dict, _printed: set, max_length=1500):\n current_state = event.get(\"dialog_state\")\n if current_state:\n print(\"Currently in: \", current_state[-1])\n message = event.get(\"messages\")\n if message:\n if isinstance(message, list):\n message = message[-1]\n if message.id not in _printed:\n msg_repr = message.pretty_repr(html=True)\n if len(msg_repr) > max_length:\n msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n print(msg_repr)\n _printed.add(message.id)"]
+ "source": [
+ "from langchain_core.messages import ToolMessage\n",
+ "from langchain_core.runnables import RunnableLambda\n",
+ "\n",
+ "from langgraph.prebuilt import ToolNode\n",
+ "\n",
+ "\n",
+ "def handle_tool_error(state) -> dict:\n",
+ " error = state.get(\"error\")\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " return {\n",
+ " \"messages\": [\n",
+ " ToolMessage(\n",
+ " content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n",
+ " tool_call_id=tc[\"id\"],\n",
+ " )\n",
+ " for tc in tool_calls\n",
+ " ]\n",
+ " }\n",
+ "\n",
+ "\n",
+ "def create_tool_node_with_fallback(tools: list) -> dict:\n",
+ " return ToolNode(tools).with_fallbacks(\n",
+ " [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def _print_event(event: dict, _printed: set, max_length=1500):\n",
+ " current_state = event.get(\"dialog_state\")\n",
+ " if current_state:\n",
+ " print(\"Currently in: \", current_state[-1])\n",
+ " message = event.get(\"messages\")\n",
+ " if message:\n",
+ " if isinstance(message, list):\n",
+ " message = message[-1]\n",
+ " if message.id not in _printed:\n",
+ " msg_repr = message.pretty_repr(html=True)\n",
+ " if len(msg_repr) > max_length:\n",
+ " msg_repr = msg_repr[:max_length] + \" ... (truncated)\"\n",
+ " print(msg_repr)\n",
+ " _printed.add(message.id)"
+ ]
},
{
"cell_type": "markdown",
@@ -203,7 +973,17 @@
"id": "a3216948",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph.message import AnyMessage, add_messages\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list[AnyMessage], add_messages]"
+ ]
},
{
"cell_type": "markdown",
@@ -230,7 +1010,83 @@
]
}
],
- "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import Runnable, RunnableConfig\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n configuration = config.get(\"configurable\", {})\n passenger_id = configuration.get(\"passenger_id\", None)\n state = {**state, \"user_info\": passenger_id}\n result = self.runnable.invoke(state)\n # If the LLM happens to return an empty response, we will re-prompt it\n # for an actual response.\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\n# Haiku is faster and cheaper, but less accurate\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n# You could swap LLMs, though you will likely want to update the prompts when\n# doing so!\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nprimary_assistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\npart_1_tools = [\n TavilySearchResults(max_results=1),\n fetch_user_flight_information,\n search_flights,\n lookup_policy,\n update_ticket_to_new_flight,\n cancel_ticket,\n search_car_rentals,\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n search_hotels,\n book_hotel,\n update_hotel,\n cancel_hotel,\n search_trip_recommendations,\n book_excursion,\n update_excursion,\n cancel_excursion,\n]\npart_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)"]
+ "source": [
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.runnables import Runnable, RunnableConfig\n",
+ "\n",
+ "\n",
+ "class Assistant:\n",
+ " def __init__(self, runnable: Runnable):\n",
+ " self.runnable = runnable\n",
+ "\n",
+ " def __call__(self, state: State, config: RunnableConfig):\n",
+ " while True:\n",
+ " configuration = config.get(\"configurable\", {})\n",
+ " passenger_id = configuration.get(\"passenger_id\", None)\n",
+ " state = {**state, \"user_info\": passenger_id}\n",
+ " result = self.runnable.invoke(state)\n",
+ " # If the LLM happens to return an empty response, we will re-prompt it\n",
+ " # for an actual response.\n",
+ " if not result.tool_calls and (\n",
+ " not result.content\n",
+ " or isinstance(result.content, list)\n",
+ " and not result.content[0].get(\"text\")\n",
+ " ):\n",
+ " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n",
+ " state = {**state, \"messages\": messages}\n",
+ " else:\n",
+ " break\n",
+ " return {\"messages\": result}\n",
+ "\n",
+ "\n",
+ "# Haiku is faster and cheaper, but less accurate\n",
+ "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n",
+ "# You could swap LLMs, though you will likely want to update the prompts when\n",
+ "# doing so!\n",
+ "# from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
+ "\n",
+ "primary_assistant_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a helpful customer support assistant for Swiss Airlines. \"\n",
+ " \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \" If a search comes up empty, expand your search before giving up.\"\n",
+ " \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n",
+ " \"\\nCurrent time: {time}.\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "part_1_tools = [\n",
+ " TavilySearchResults(max_results=1),\n",
+ " fetch_user_flight_information,\n",
+ " search_flights,\n",
+ " lookup_policy,\n",
+ " update_ticket_to_new_flight,\n",
+ " cancel_ticket,\n",
+ " search_car_rentals,\n",
+ " book_car_rental,\n",
+ " update_car_rental,\n",
+ " cancel_car_rental,\n",
+ " search_hotels,\n",
+ " book_hotel,\n",
+ " update_hotel,\n",
+ " cancel_hotel,\n",
+ " search_trip_recommendations,\n",
+ " book_excursion,\n",
+ " update_excursion,\n",
+ " cancel_excursion,\n",
+ "]\n",
+ "part_1_assistant_runnable = primary_assistant_prompt | llm.bind_tools(part_1_tools)"
+ ]
},
{
"cell_type": "markdown",
@@ -248,7 +1104,30 @@
"id": "36064ee6",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\n# Define nodes: these do the work\nbuilder.add_node(\"assistant\", Assistant(part_1_assistant_runnable))\nbuilder.add_node(\"tools\", create_tool_node_with_fallback(part_1_tools))\n# Define edges: these determine how the control flow moves\nbuilder.add_edge(START, \"assistant\")\nbuilder.add_conditional_edges(\n \"assistant\",\n tools_condition,\n)\nbuilder.add_edge(\"tools\", \"assistant\")\n\n# The checkpointer lets the graph persist its state\n# this is a complete memory for the entire graph.\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_1_graph = builder.compile(checkpointer=memory)"]
+ "source": [
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "from langgraph.graph import END, StateGraph, START\n",
+ "from langgraph.prebuilt import tools_condition\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "# Define nodes: these do the work\n",
+ "builder.add_node(\"assistant\", Assistant(part_1_assistant_runnable))\n",
+ "builder.add_node(\"tools\", create_tool_node_with_fallback(part_1_tools))\n",
+ "# Define edges: these determine how the control flow moves\n",
+ "builder.add_edge(START, \"assistant\")\n",
+ "builder.add_conditional_edges(\n",
+ " \"assistant\",\n",
+ " tools_condition,\n",
+ ")\n",
+ "builder.add_edge(\"tools\", \"assistant\")\n",
+ "\n",
+ "# The checkpointer lets the graph persist its state\n",
+ "# this is a complete memory for the entire graph.\n",
+ "memory = MemorySaver()\n",
+ "part_1_graph = builder.compile(checkpointer=memory)"
+ ]
},
{
"cell_type": "code",
@@ -267,7 +1146,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(part_1_graph.get_graph(xray=True).draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -834,7 +1721,51 @@
]
}
],
- "source": ["import shutil\nimport uuid\n\n# Let's create an example conversation a user might have with the assistant\ntutorial_questions = [\n \"Hi there, what time is my flight?\",\n \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n \"Update my flight to sometime next week then\",\n \"The next available option is great\",\n \"what about lodging and transportation?\",\n \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n \"yes go ahead and book anything that's moderate expense and has availability.\",\n \"Now for a car, what are my options?\",\n \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n \"Cool so now what recommendations do you have on excursions?\",\n \"Are they available while I'm there?\",\n \"interesting - i like the museums, what options are there? \",\n \"OK great pick one and book it for my second day there.\",\n]\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\n\n_printed = set()\nfor question in tutorial_questions:\n events = part_1_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)"]
+ "source": [
+ "import shutil\n",
+ "import uuid\n",
+ "\n",
+ "# Let's create an example conversation a user might have with the assistant\n",
+ "tutorial_questions = [\n",
+ " \"Hi there, what time is my flight?\",\n",
+ " \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n",
+ " \"Update my flight to sometime next week then\",\n",
+ " \"The next available option is great\",\n",
+ " \"what about lodging and transportation?\",\n",
+ " \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n",
+ " \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n",
+ " \"yes go ahead and book anything that's moderate expense and has availability.\",\n",
+ " \"Now for a car, what are my options?\",\n",
+ " \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n",
+ " \"Cool so now what recommendations do you have on excursions?\",\n",
+ " \"Are they available while I'm there?\",\n",
+ " \"interesting - i like the museums, what options are there? \",\n",
+ " \"OK great pick one and book it for my second day there.\",\n",
+ "]\n",
+ "\n",
+ "# Update with the backup file so we can restart from the original place in each section\n",
+ "shutil.copy(backup_file, db)\n",
+ "thread_id = str(uuid.uuid4())\n",
+ "\n",
+ "config = {\n",
+ " \"configurable\": {\n",
+ " # The passenger_id is used in our flight tools to\n",
+ " # fetch the user's flight information\n",
+ " \"passenger_id\": \"3442 587242\",\n",
+ " # Checkpoints are accessed by thread_id\n",
+ " \"thread_id\": thread_id,\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "_printed = set()\n",
+ "for question in tutorial_questions:\n",
+ " events = part_1_graph.stream(\n",
+ " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n",
+ " )\n",
+ " for event in events:\n",
+ " _print_event(event, _printed)"
+ ]
},
{
"cell_type": "markdown",
@@ -887,7 +1818,90 @@
"id": "c5098273-e1f6-46bf-b63b-172bbd3d9104",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import Runnable, RunnableConfig\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n user_info: str\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n result = self.runnable.invoke(state)\n # If the LLM happens to return an empty response, we will re-prompt it\n # for an actual response.\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\n# Haiku is faster and cheaper, but less accurate\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n# You could also use OpenAI or another model, though you will likely have\n# to adapt the prompts\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nassistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\npart_2_tools = [\n TavilySearchResults(max_results=1),\n fetch_user_flight_information,\n search_flights,\n lookup_policy,\n update_ticket_to_new_flight,\n cancel_ticket,\n search_car_rentals,\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n search_hotels,\n book_hotel,\n update_hotel,\n cancel_hotel,\n search_trip_recommendations,\n book_excursion,\n update_excursion,\n cancel_excursion,\n]\npart_2_assistant_runnable = assistant_prompt | llm.bind_tools(part_2_tools)"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.runnables import Runnable, RunnableConfig\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph.message import AnyMessage, add_messages\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list[AnyMessage], add_messages]\n",
+ " user_info: str\n",
+ "\n",
+ "\n",
+ "class Assistant:\n",
+ " def __init__(self, runnable: Runnable):\n",
+ " self.runnable = runnable\n",
+ "\n",
+ " def __call__(self, state: State, config: RunnableConfig):\n",
+ " while True:\n",
+ " result = self.runnable.invoke(state)\n",
+ " # If the LLM happens to return an empty response, we will re-prompt it\n",
+ " # for an actual response.\n",
+ " if not result.tool_calls and (\n",
+ " not result.content\n",
+ " or isinstance(result.content, list)\n",
+ " and not result.content[0].get(\"text\")\n",
+ " ):\n",
+ " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n",
+ " state = {**state, \"messages\": messages}\n",
+ " else:\n",
+ " break\n",
+ " return {\"messages\": result}\n",
+ "\n",
+ "\n",
+ "# Haiku is faster and cheaper, but less accurate\n",
+ "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n",
+ "# You could also use OpenAI or another model, though you will likely have\n",
+ "# to adapt the prompts\n",
+ "# from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
+ "\n",
+ "assistant_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a helpful customer support assistant for Swiss Airlines. \"\n",
+ " \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \" If a search comes up empty, expand your search before giving up.\"\n",
+ " \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n",
+ " \"\\nCurrent time: {time}.\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "part_2_tools = [\n",
+ " TavilySearchResults(max_results=1),\n",
+ " fetch_user_flight_information,\n",
+ " search_flights,\n",
+ " lookup_policy,\n",
+ " update_ticket_to_new_flight,\n",
+ " cancel_ticket,\n",
+ " search_car_rentals,\n",
+ " book_car_rental,\n",
+ " update_car_rental,\n",
+ " cancel_car_rental,\n",
+ " search_hotels,\n",
+ " book_hotel,\n",
+ " update_hotel,\n",
+ " cancel_hotel,\n",
+ " search_trip_recommendations,\n",
+ " book_excursion,\n",
+ " update_excursion,\n",
+ " cancel_excursion,\n",
+ "]\n",
+ "part_2_assistant_runnable = assistant_prompt | llm.bind_tools(part_2_tools)"
+ ]
},
{
"cell_type": "markdown",
@@ -908,7 +1922,40 @@
"id": "910002ce-2431-4280-854a-a273c517611b",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\ndef user_info(state: State):\n return {\"user_info\": fetch_user_flight_information.invoke({})}\n\n\n# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n# having to take an action\nbuilder.add_node(\"fetch_user_info\", user_info)\nbuilder.add_edge(START, \"fetch_user_info\")\nbuilder.add_node(\"assistant\", Assistant(part_2_assistant_runnable))\nbuilder.add_node(\"tools\", create_tool_node_with_fallback(part_2_tools))\nbuilder.add_edge(\"fetch_user_info\", \"assistant\")\nbuilder.add_conditional_edges(\n \"assistant\",\n tools_condition,\n)\nbuilder.add_edge(\"tools\", \"assistant\")\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_2_graph = builder.compile(\n checkpointer=memory,\n # NEW: The graph will always halt before executing the \"tools\" node.\n # The user can approve or reject (or even alter the request) before\n # the assistant continues\n interrupt_before=[\"tools\"],\n)"]
+ "source": [
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "from langgraph.graph import StateGraph\n",
+ "from langgraph.prebuilt import tools_condition\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "def user_info(state: State):\n",
+ " return {\"user_info\": fetch_user_flight_information.invoke({})}\n",
+ "\n",
+ "\n",
+ "# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n",
+ "# having to take an action\n",
+ "builder.add_node(\"fetch_user_info\", user_info)\n",
+ "builder.add_edge(START, \"fetch_user_info\")\n",
+ "builder.add_node(\"assistant\", Assistant(part_2_assistant_runnable))\n",
+ "builder.add_node(\"tools\", create_tool_node_with_fallback(part_2_tools))\n",
+ "builder.add_edge(\"fetch_user_info\", \"assistant\")\n",
+ "builder.add_conditional_edges(\n",
+ " \"assistant\",\n",
+ " tools_condition,\n",
+ ")\n",
+ "builder.add_edge(\"tools\", \"assistant\")\n",
+ "\n",
+ "memory = MemorySaver()\n",
+ "part_2_graph = builder.compile(\n",
+ " checkpointer=memory,\n",
+ " # NEW: The graph will always halt before executing the \"tools\" node.\n",
+ " # The user can approve or reject (or even alter the request) before\n",
+ " # the assistant continues\n",
+ " interrupt_before=[\"tools\"],\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -927,7 +1974,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_2_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(part_2_graph.get_graph(xray=True).draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -1238,7 +2293,64 @@
]
}
],
- "source": ["import shutil\nimport uuid\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\n\n_printed = set()\n# We can reuse the tutorial questions from part 1 to see how it does.\nfor question in tutorial_questions:\n events = part_2_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)\n snapshot = part_2_graph.get_state(config)\n while snapshot.next:\n # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n user_input = input(\n \"Do you approve of the above actions? Type 'y' to continue;\"\n \" otherwise, explain your requested changed.\\n\\n\"\n )\n if user_input.strip() == \"y\":\n # Just continue\n result = part_2_graph.invoke(\n None,\n config,\n )\n else:\n # Satisfy the tool invocation by\n # providing instructions on the requested changes / change of mind\n result = part_2_graph.invoke(\n {\n \"messages\": [\n ToolMessage(\n tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n )\n ]\n },\n config,\n )\n snapshot = part_2_graph.get_state(config)"]
+ "source": [
+ "import shutil\n",
+ "import uuid\n",
+ "\n",
+ "# Update with the backup file so we can restart from the original place in each section\n",
+ "shutil.copy(backup_file, db)\n",
+ "thread_id = str(uuid.uuid4())\n",
+ "\n",
+ "config = {\n",
+ " \"configurable\": {\n",
+ " # The passenger_id is used in our flight tools to\n",
+ " # fetch the user's flight information\n",
+ " \"passenger_id\": \"3442 587242\",\n",
+ " # Checkpoints are accessed by thread_id\n",
+ " \"thread_id\": thread_id,\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "\n",
+ "_printed = set()\n",
+ "# We can reuse the tutorial questions from part 1 to see how it does.\n",
+ "for question in tutorial_questions:\n",
+ " events = part_2_graph.stream(\n",
+ " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n",
+ " )\n",
+ " for event in events:\n",
+ " _print_event(event, _printed)\n",
+ " snapshot = part_2_graph.get_state(config)\n",
+ " while snapshot.next:\n",
+ " # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n",
+ " # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n",
+ " # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n",
+ " user_input = input(\n",
+ " \"Do you approve of the above actions? Type 'y' to continue;\"\n",
+ " \" otherwise, explain your requested changed.\\n\\n\"\n",
+ " )\n",
+ " if user_input.strip() == \"y\":\n",
+ " # Just continue\n",
+ " result = part_2_graph.invoke(\n",
+ " None,\n",
+ " config,\n",
+ " )\n",
+ " else:\n",
+ " # Satisfy the tool invocation by\n",
+ " # providing instructions on the requested changes / change of mind\n",
+ " result = part_2_graph.invoke(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " ToolMessage(\n",
+ " tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n",
+ " content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n",
+ " )\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " )\n",
+ " snapshot = part_2_graph.get_state(config)"
+ ]
},
{
"cell_type": "markdown",
@@ -1283,7 +2395,102 @@
"id": "20f99193-9195-42ae-8df1-0cf1489a164c",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.runnables import Runnable, RunnableConfig\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n user_info: str\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n result = self.runnable.invoke(state)\n # If the LLM happens to return an empty response, we will re-prompt it\n # for an actual response.\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\n# Haiku is faster and cheaper, but less accurate\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n# You can update the LLMs, though you may need to update the prompts\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n\nassistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\n\n# \"Read\"-only tools (such as retrievers) don't need a user confirmation to use\npart_3_safe_tools = [\n TavilySearchResults(max_results=1),\n fetch_user_flight_information,\n search_flights,\n lookup_policy,\n search_car_rentals,\n search_hotels,\n search_trip_recommendations,\n]\n\n# These tools all change the user's reservations.\n# The user has the right to control what decisions are made\npart_3_sensitive_tools = [\n update_ticket_to_new_flight,\n cancel_ticket,\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n book_hotel,\n update_hotel,\n cancel_hotel,\n book_excursion,\n update_excursion,\n cancel_excursion,\n]\nsensitive_tool_names = {t.name for t in part_3_sensitive_tools}\n# Our LLM doesn't have to know which nodes it has to route to. In its 'mind', it's just invoking functions.\npart_3_assistant_runnable = assistant_prompt | llm.bind_tools(\n part_3_safe_tools + part_3_sensitive_tools\n)"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.runnables import Runnable, RunnableConfig\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph.message import AnyMessage, add_messages\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list[AnyMessage], add_messages]\n",
+ " user_info: str\n",
+ "\n",
+ "\n",
+ "class Assistant:\n",
+ " def __init__(self, runnable: Runnable):\n",
+ " self.runnable = runnable\n",
+ "\n",
+ " def __call__(self, state: State, config: RunnableConfig):\n",
+ " while True:\n",
+ " result = self.runnable.invoke(state)\n",
+ " # If the LLM happens to return an empty response, we will re-prompt it\n",
+ " # for an actual response.\n",
+ " if not result.tool_calls and (\n",
+ " not result.content\n",
+ " or isinstance(result.content, list)\n",
+ " and not result.content[0].get(\"text\")\n",
+ " ):\n",
+ " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n",
+ " state = {**state, \"messages\": messages}\n",
+ " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n",
+ " state = {**state, \"messages\": messages}\n",
+ " else:\n",
+ " break\n",
+ " return {\"messages\": result}\n",
+ "\n",
+ "\n",
+ "# Haiku is faster and cheaper, but less accurate\n",
+ "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n",
+ "# You can update the LLMs, though you may need to update the prompts\n",
+ "# from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n",
+ "\n",
+ "assistant_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a helpful customer support assistant for Swiss Airlines. \"\n",
+ " \" Use the provided tools to search for flights, company policies, and other information to assist the user's queries. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \" If a search comes up empty, expand your search before giving up.\"\n",
+ " \"\\n\\nCurrent user:\\n\\n{user_info}\\n\"\n",
+ " \"\\nCurrent time: {time}.\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "\n",
+ "# \"Read\"-only tools (such as retrievers) don't need a user confirmation to use\n",
+ "part_3_safe_tools = [\n",
+ " TavilySearchResults(max_results=1),\n",
+ " fetch_user_flight_information,\n",
+ " search_flights,\n",
+ " lookup_policy,\n",
+ " search_car_rentals,\n",
+ " search_hotels,\n",
+ " search_trip_recommendations,\n",
+ "]\n",
+ "\n",
+ "# These tools all change the user's reservations.\n",
+ "# The user has the right to control what decisions are made\n",
+ "part_3_sensitive_tools = [\n",
+ " update_ticket_to_new_flight,\n",
+ " cancel_ticket,\n",
+ " book_car_rental,\n",
+ " update_car_rental,\n",
+ " cancel_car_rental,\n",
+ " book_hotel,\n",
+ " update_hotel,\n",
+ " cancel_hotel,\n",
+ " book_excursion,\n",
+ " update_excursion,\n",
+ " cancel_excursion,\n",
+ "]\n",
+ "sensitive_tool_names = {t.name for t in part_3_sensitive_tools}\n",
+ "# Our LLM doesn't have to know which nodes it has to route to. In its 'mind', it's just invoking functions.\n",
+ "part_3_assistant_runnable = assistant_prompt | llm.bind_tools(\n",
+ " part_3_safe_tools + part_3_sensitive_tools\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -1301,7 +2508,63 @@
"id": "928b756f-2934-4b1b-95d1-0c4f974b978f",
"metadata": {},
"outputs": [],
- "source": ["from typing import Literal\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\ndef user_info(state: State):\n return {\"user_info\": fetch_user_flight_information.invoke({})}\n\n\n# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n# having to take an action\nbuilder.add_node(\"fetch_user_info\", user_info)\nbuilder.add_edge(START, \"fetch_user_info\")\nbuilder.add_node(\"assistant\", Assistant(part_3_assistant_runnable))\nbuilder.add_node(\"safe_tools\", create_tool_node_with_fallback(part_3_safe_tools))\nbuilder.add_node(\n \"sensitive_tools\", create_tool_node_with_fallback(part_3_sensitive_tools)\n)\n# Define logic\nbuilder.add_edge(\"fetch_user_info\", \"assistant\")\n\n\ndef route_tools(state: State) -> Literal[\"safe_tools\", \"sensitive_tools\", \"__end__\"]:\n next_node = tools_condition(state)\n # If no tools are invoked, return to the user\n if next_node == END:\n return END\n ai_message = state[\"messages\"][-1]\n # This assumes single tool calls. To handle parallel tool calling, you'd want to\n # use an ANY condition\n first_tool_call = ai_message.tool_calls[0]\n if first_tool_call[\"name\"] in sensitive_tool_names:\n return \"sensitive_tools\"\n return \"safe_tools\"\n\n\nbuilder.add_conditional_edges(\n \"assistant\",\n route_tools,\n)\nbuilder.add_edge(\"safe_tools\", \"assistant\")\nbuilder.add_edge(\"sensitive_tools\", \"assistant\")\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_3_graph = builder.compile(\n checkpointer=memory,\n # NEW: The graph will always halt before executing the \"tools\" node.\n # The user can approve or reject (or even alter the request) before\n # the assistant continues\n interrupt_before=[\"sensitive_tools\"],\n)"]
+ "source": [
+ "from typing import Literal\n",
+ "\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "from langgraph.graph import StateGraph\n",
+ "from langgraph.prebuilt import tools_condition\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "def user_info(state: State):\n",
+ " return {\"user_info\": fetch_user_flight_information.invoke({})}\n",
+ "\n",
+ "\n",
+ "# NEW: The fetch_user_info node runs first, meaning our assistant can see the user's flight information without\n",
+ "# having to take an action\n",
+ "builder.add_node(\"fetch_user_info\", user_info)\n",
+ "builder.add_edge(START, \"fetch_user_info\")\n",
+ "builder.add_node(\"assistant\", Assistant(part_3_assistant_runnable))\n",
+ "builder.add_node(\"safe_tools\", create_tool_node_with_fallback(part_3_safe_tools))\n",
+ "builder.add_node(\n",
+ " \"sensitive_tools\", create_tool_node_with_fallback(part_3_sensitive_tools)\n",
+ ")\n",
+ "# Define logic\n",
+ "builder.add_edge(\"fetch_user_info\", \"assistant\")\n",
+ "\n",
+ "\n",
+ "def route_tools(state: State) -> Literal[\"safe_tools\", \"sensitive_tools\", \"__end__\"]:\n",
+ " next_node = tools_condition(state)\n",
+ " # If no tools are invoked, return to the user\n",
+ " if next_node == END:\n",
+ " return END\n",
+ " ai_message = state[\"messages\"][-1]\n",
+ " # This assumes single tool calls. To handle parallel tool calling, you'd want to\n",
+ " # use an ANY condition\n",
+ " first_tool_call = ai_message.tool_calls[0]\n",
+ " if first_tool_call[\"name\"] in sensitive_tool_names:\n",
+ " return \"sensitive_tools\"\n",
+ " return \"safe_tools\"\n",
+ "\n",
+ "\n",
+ "builder.add_conditional_edges(\n",
+ " \"assistant\",\n",
+ " route_tools,\n",
+ ")\n",
+ "builder.add_edge(\"safe_tools\", \"assistant\")\n",
+ "builder.add_edge(\"sensitive_tools\", \"assistant\")\n",
+ "\n",
+ "memory = MemorySaver()\n",
+ "part_3_graph = builder.compile(\n",
+ " checkpointer=memory,\n",
+ " # NEW: The graph will always halt before executing the \"tools\" node.\n",
+ " # The user can approve or reject (or even alter the request) before\n",
+ " # the assistant continues\n",
+ " interrupt_before=[\"sensitive_tools\"],\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -1320,7 +2583,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_3_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(part_3_graph.get_graph(xray=True).draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -1626,7 +2897,81 @@
]
}
],
- "source": ["import shutil\nimport uuid\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\ntutorial_questions = [\n \"Hi there, what time is my flight?\",\n \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n \"Update my flight to sometime next week then\",\n \"The next available option is great\",\n \"what about lodging and transportation?\",\n \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n \"yes go ahead and book anything that's moderate expense and has availability.\",\n \"Now for a car, what are my options?\",\n \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n \"Cool so now what recommendations do you have on excursions?\",\n \"Are they available while I'm there?\",\n \"interesting - i like the museums, what options are there? \",\n \"OK great pick one and book it for my second day there.\",\n]\n\n\n_printed = set()\n# We can reuse the tutorial questions from part 1 to see how it does.\nfor question in tutorial_questions:\n events = part_3_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)\n snapshot = part_3_graph.get_state(config)\n while snapshot.next:\n # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n user_input = input(\n \"Do you approve of the above actions? Type 'y' to continue;\"\n \" otherwise, explain your requested changed.\\n\\n\"\n )\n if user_input.strip() == \"y\":\n # Just continue\n result = part_3_graph.invoke(\n None,\n config,\n )\n else:\n # Satisfy the tool invocation by\n # providing instructions on the requested changes / change of mind\n result = part_3_graph.invoke(\n {\n \"messages\": [\n ToolMessage(\n tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n )\n ]\n },\n config,\n )\n snapshot = part_3_graph.get_state(config)"]
+ "source": [
+ "import shutil\n",
+ "import uuid\n",
+ "\n",
+ "# Update with the backup file so we can restart from the original place in each section\n",
+ "shutil.copy(backup_file, db)\n",
+ "thread_id = str(uuid.uuid4())\n",
+ "\n",
+ "config = {\n",
+ " \"configurable\": {\n",
+ " # The passenger_id is used in our flight tools to\n",
+ " # fetch the user's flight information\n",
+ " \"passenger_id\": \"3442 587242\",\n",
+ " # Checkpoints are accessed by thread_id\n",
+ " \"thread_id\": thread_id,\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "tutorial_questions = [\n",
+ " \"Hi there, what time is my flight?\",\n",
+ " \"Am i allowed to update my flight to something sooner? I want to leave later today.\",\n",
+ " \"Update my flight to sometime next week then\",\n",
+ " \"The next available option is great\",\n",
+ " \"what about lodging and transportation?\",\n",
+ " \"Yeah i think i'd like an affordable hotel for my week-long stay (7 days). And I'll want to rent a car.\",\n",
+ " \"OK could you place a reservation for your recommended hotel? It sounds nice.\",\n",
+ " \"yes go ahead and book anything that's moderate expense and has availability.\",\n",
+ " \"Now for a car, what are my options?\",\n",
+ " \"Awesome let's just get the cheapest option. Go ahead and book for 7 days\",\n",
+ " \"Cool so now what recommendations do you have on excursions?\",\n",
+ " \"Are they available while I'm there?\",\n",
+ " \"interesting - i like the museums, what options are there? \",\n",
+ " \"OK great pick one and book it for my second day there.\",\n",
+ "]\n",
+ "\n",
+ "\n",
+ "_printed = set()\n",
+ "# We can reuse the tutorial questions from part 1 to see how it does.\n",
+ "for question in tutorial_questions:\n",
+ " events = part_3_graph.stream(\n",
+ " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n",
+ " )\n",
+ " for event in events:\n",
+ " _print_event(event, _printed)\n",
+ " snapshot = part_3_graph.get_state(config)\n",
+ " while snapshot.next:\n",
+ " # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n",
+ " # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n",
+ " # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n",
+ " user_input = input(\n",
+ " \"Do you approve of the above actions? Type 'y' to continue;\"\n",
+ " \" otherwise, explain your requested changed.\\n\\n\"\n",
+ " )\n",
+ " if user_input.strip() == \"y\":\n",
+ " # Just continue\n",
+ " result = part_3_graph.invoke(\n",
+ " None,\n",
+ " config,\n",
+ " )\n",
+ " else:\n",
+ " # Satisfy the tool invocation by\n",
+ " # providing instructions on the requested changes / change of mind\n",
+ " result = part_3_graph.invoke(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " ToolMessage(\n",
+ " tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n",
+ " content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n",
+ " )\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " )\n",
+ " snapshot = part_3_graph.get_state(config)"
+ ]
},
{
"cell_type": "markdown",
@@ -1672,7 +3017,39 @@
"id": "2997e1f9-3a4b-4794-b71f-992da3a644fa",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated, Literal, Optional\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\ndef update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:\n \"\"\"Push or pop the state.\"\"\"\n if right is None:\n return left\n if right == \"pop\":\n return left[:-1]\n return left + [right]\n\n\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n user_info: str\n dialog_state: Annotated[\n list[\n Literal[\n \"assistant\",\n \"update_flight\",\n \"book_car_rental\",\n \"book_hotel\",\n \"book_excursion\",\n ]\n ],\n update_dialog_stack,\n ]"]
+ "source": [
+ "from typing import Annotated, Literal, Optional\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph.message import AnyMessage, add_messages\n",
+ "\n",
+ "\n",
+ "def update_dialog_stack(left: list[str], right: Optional[str]) -> list[str]:\n",
+ " \"\"\"Push or pop the state.\"\"\"\n",
+ " if right is None:\n",
+ " return left\n",
+ " if right == \"pop\":\n",
+ " return left[:-1]\n",
+ " return left + [right]\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list[AnyMessage], add_messages]\n",
+ " user_info: str\n",
+ " dialog_state: Annotated[\n",
+ " list[\n",
+ " Literal[\n",
+ " \"assistant\",\n",
+ " \"update_flight\",\n",
+ " \"book_car_rental\",\n",
+ " \"book_hotel\",\n",
+ " \"book_excursion\",\n",
+ " ]\n",
+ " ],\n",
+ " update_dialog_stack,\n",
+ " ]"
+ ]
},
{
"cell_type": "markdown",
@@ -1702,7 +3079,301 @@
"id": "1ef67c85-b999-406c-a745-09fdc0dfa0b3",
"metadata": {},
"outputs": [],
- "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_core.runnables import Runnable, RunnableConfig\n\n\nclass Assistant:\n def __init__(self, runnable: Runnable):\n self.runnable = runnable\n\n def __call__(self, state: State, config: RunnableConfig):\n while True:\n result = self.runnable.invoke(state)\n\n if not result.tool_calls and (\n not result.content\n or isinstance(result.content, list)\n and not result.content[0].get(\"text\")\n ):\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n state = {**state, \"messages\": messages}\n else:\n break\n return {\"messages\": result}\n\n\nclass CompleteOrEscalate(BaseModel):\n \"\"\"A tool to mark the current task as completed and/or to escalate control of the dialog to the main assistant,\n who can re-route the dialog based on the user's needs.\"\"\"\n\n cancel: bool = True\n reason: str\n\n class Config:\n schema_extra = {\n \"example\": {\n \"cancel\": True,\n \"reason\": \"User changed their mind about the current task.\",\n },\n \"example 2\": {\n \"cancel\": True,\n \"reason\": \"I have fully completed the task.\",\n },\n \"example 3\": {\n \"cancel\": False,\n \"reason\": \"I need to search the user's emails or calendar for more information.\",\n },\n }\n\n\n# Flight booking assistant\n\nflight_booking_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling flight updates. \"\n \" The primary assistant delegates work to you whenever the user needs help updating their bookings. \"\n \"Confirm the updated flight details with the customer and inform them of any additional fees. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\"\n \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then\"\n ' \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.',\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nupdate_flight_safe_tools = [search_flights]\nupdate_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]\nupdate_flight_tools = update_flight_safe_tools + update_flight_sensitive_tools\nupdate_flight_runnable = flight_booking_prompt | llm.bind_tools(\n update_flight_tools + [CompleteOrEscalate]\n)\n\n# Hotel Booking Assistant\nbook_hotel_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling hotel bookings. \"\n \"The primary assistant delegates work to you whenever the user needs help booking a hotel. \"\n \"Search for available hotels based on the user's preferences and confirm the booking details with the customer. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\nCurrent time: {time}.\"\n '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant.'\n \" Do not waste the user's time. Do not make up invalid tools or functions.\"\n \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n \" - 'what's the weather like this time of year?'\\n\"\n \" - 'nevermind i think I'll book separately'\\n\"\n \" - 'i need to figure out transportation while i'm there'\\n\"\n \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n \" - 'Hotel booking confirmed'\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nbook_hotel_safe_tools = [search_hotels]\nbook_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]\nbook_hotel_tools = book_hotel_safe_tools + book_hotel_sensitive_tools\nbook_hotel_runnable = book_hotel_prompt | llm.bind_tools(\n book_hotel_tools + [CompleteOrEscalate]\n)\n\n# Car Rental Assistant\nbook_car_rental_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling car rental bookings. \"\n \"The primary assistant delegates work to you whenever the user needs help booking a car rental. \"\n \"Search for available car rentals based on the user's preferences and confirm the booking details with the customer. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\nCurrent time: {time}.\"\n \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"\n '\"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n \" - 'what's the weather like this time of year?'\\n\"\n \" - 'What flights are available?'\\n\"\n \" - 'nevermind i think I'll book separately'\\n\"\n \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n \" - 'Car rental booking confirmed'\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nbook_car_rental_safe_tools = [search_car_rentals]\nbook_car_rental_sensitive_tools = [\n book_car_rental,\n update_car_rental,\n cancel_car_rental,\n]\nbook_car_rental_tools = book_car_rental_safe_tools + book_car_rental_sensitive_tools\nbook_car_rental_runnable = book_car_rental_prompt | llm.bind_tools(\n book_car_rental_tools + [CompleteOrEscalate]\n)\n\n# Excursion Assistant\n\nbook_excursion_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a specialized assistant for handling trip recommendations. \"\n \"The primary assistant delegates work to you whenever the user needs help booking a recommended trip. \"\n \"Search for available trip recommendations based on the user's preferences and confirm the booking details with the customer. \"\n \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n \"\\nCurrent time: {time}.\"\n '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n \" - 'nevermind i think I'll book separately'\\n\"\n \" - 'i need to figure out transportation while i'm there'\\n\"\n \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n \" - 'Excursion booking confirmed!'\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\n\nbook_excursion_safe_tools = [search_trip_recommendations]\nbook_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]\nbook_excursion_tools = book_excursion_safe_tools + book_excursion_sensitive_tools\nbook_excursion_runnable = book_excursion_prompt | llm.bind_tools(\n book_excursion_tools + [CompleteOrEscalate]\n)\n\n\n# Primary Assistant\nclass ToFlightBookingAssistant(BaseModel):\n \"\"\"Transfers work to a specialized assistant to handle flight updates and cancellations.\"\"\"\n\n request: str = Field(\n description=\"Any necessary followup questions the update flight assistant should clarify before proceeding.\"\n )\n\n\nclass ToBookCarRental(BaseModel):\n \"\"\"Transfers work to a specialized assistant to handle car rental bookings.\"\"\"\n\n location: str = Field(\n description=\"The location where the user wants to rent a car.\"\n )\n start_date: str = Field(description=\"The start date of the car rental.\")\n end_date: str = Field(description=\"The end date of the car rental.\")\n request: str = Field(\n description=\"Any additional information or requests from the user regarding the car rental.\"\n )\n\n class Config:\n schema_extra = {\n \"example\": {\n \"location\": \"Basel\",\n \"start_date\": \"2023-07-01\",\n \"end_date\": \"2023-07-05\",\n \"request\": \"I need a compact car with automatic transmission.\",\n }\n }\n\n\nclass ToHotelBookingAssistant(BaseModel):\n \"\"\"Transfer work to a specialized assistant to handle hotel bookings.\"\"\"\n\n location: str = Field(\n description=\"The location where the user wants to book a hotel.\"\n )\n checkin_date: str = Field(description=\"The check-in date for the hotel.\")\n checkout_date: str = Field(description=\"The check-out date for the hotel.\")\n request: str = Field(\n description=\"Any additional information or requests from the user regarding the hotel booking.\"\n )\n\n class Config:\n schema_extra = {\n \"example\": {\n \"location\": \"Zurich\",\n \"checkin_date\": \"2023-08-15\",\n \"checkout_date\": \"2023-08-20\",\n \"request\": \"I prefer a hotel near the city center with a room that has a view.\",\n }\n }\n\n\nclass ToBookExcursion(BaseModel):\n \"\"\"Transfers work to a specialized assistant to handle trip recommendation and other excursion bookings.\"\"\"\n\n location: str = Field(\n description=\"The location where the user wants to book a recommended trip.\"\n )\n request: str = Field(\n description=\"Any additional information or requests from the user regarding the trip recommendation.\"\n )\n\n class Config:\n schema_extra = {\n \"example\": {\n \"location\": \"Lucerne\",\n \"request\": \"The user is interested in outdoor activities and scenic views.\",\n }\n }\n\n\n# The top-level assistant performs general Q&A and delegates specialized tasks to other assistants.\n# The task delegation is a simple form of semantic routing / does simple intent detection\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n\nprimary_assistant_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful customer support assistant for Swiss Airlines. \"\n \"Your primary role is to search for flight information and company policies to answer customer queries. \"\n \"If a customer requests to update or cancel a flight, book a car rental, book a hotel, or get trip recommendations, \"\n \"delegate the task to the appropriate specialized assistant by invoking the corresponding tool. You are not able to make these types of changes yourself.\"\n \" Only the specialized assistants are given permission to do this for the user.\"\n \"The user is not aware of the different specialized assistants, so do not mention them; just quietly delegate through function calls. \"\n \"Provide detailed information to the customer, and always double-check the database before concluding that information is unavailable. \"\n \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n \" If a search comes up empty, expand your search before giving up.\"\n \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n \"\\nCurrent time: {time}.\",\n ),\n (\"placeholder\", \"{messages}\"),\n ]\n).partial(time=datetime.now())\nprimary_assistant_tools = [\n TavilySearchResults(max_results=1),\n search_flights,\n lookup_policy,\n]\nassistant_runnable = primary_assistant_prompt | llm.bind_tools(\n primary_assistant_tools\n + [\n ToFlightBookingAssistant,\n ToBookCarRental,\n ToHotelBookingAssistant,\n ToBookExcursion,\n ]\n)"]
+ "source": [
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_core.runnables import Runnable, RunnableConfig\n",
+ "\n",
+ "\n",
+ "class Assistant:\n",
+ " def __init__(self, runnable: Runnable):\n",
+ " self.runnable = runnable\n",
+ "\n",
+ " def __call__(self, state: State, config: RunnableConfig):\n",
+ " while True:\n",
+ " result = self.runnable.invoke(state)\n",
+ "\n",
+ " if not result.tool_calls and (\n",
+ " not result.content\n",
+ " or isinstance(result.content, list)\n",
+ " and not result.content[0].get(\"text\")\n",
+ " ):\n",
+ " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n",
+ " state = {**state, \"messages\": messages}\n",
+ " messages = state[\"messages\"] + [(\"user\", \"Respond with a real output.\")]\n",
+ " state = {**state, \"messages\": messages}\n",
+ " else:\n",
+ " break\n",
+ " return {\"messages\": result}\n",
+ "\n",
+ "\n",
+ "class CompleteOrEscalate(BaseModel):\n",
+ " \"\"\"A tool to mark the current task as completed and/or to escalate control of the dialog to the main assistant,\n",
+ " who can re-route the dialog based on the user's needs.\"\"\"\n",
+ "\n",
+ " cancel: bool = True\n",
+ " reason: str\n",
+ "\n",
+ " class Config:\n",
+ " schema_extra = {\n",
+ " \"example\": {\n",
+ " \"cancel\": True,\n",
+ " \"reason\": \"User changed their mind about the current task.\",\n",
+ " },\n",
+ " \"example 2\": {\n",
+ " \"cancel\": True,\n",
+ " \"reason\": \"I have fully completed the task.\",\n",
+ " },\n",
+ " \"example 3\": {\n",
+ " \"cancel\": False,\n",
+ " \"reason\": \"I need to search the user's emails or calendar for more information.\",\n",
+ " },\n",
+ " }\n",
+ "\n",
+ "\n",
+ "# Flight booking assistant\n",
+ "\n",
+ "flight_booking_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a specialized assistant for handling flight updates. \"\n",
+ " \" The primary assistant delegates work to you whenever the user needs help updating their bookings. \"\n",
+ " \"Confirm the updated flight details with the customer and inform them of any additional fees. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n",
+ " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n",
+ " \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n",
+ " \"\\nCurrent time: {time}.\"\n",
+ " \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then\"\n",
+ " ' \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.',\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "update_flight_safe_tools = [search_flights]\n",
+ "update_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]\n",
+ "update_flight_tools = update_flight_safe_tools + update_flight_sensitive_tools\n",
+ "update_flight_runnable = flight_booking_prompt | llm.bind_tools(\n",
+ " update_flight_tools + [CompleteOrEscalate]\n",
+ ")\n",
+ "\n",
+ "# Hotel Booking Assistant\n",
+ "book_hotel_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a specialized assistant for handling hotel bookings. \"\n",
+ " \"The primary assistant delegates work to you whenever the user needs help booking a hotel. \"\n",
+ " \"Search for available hotels based on the user's preferences and confirm the booking details with the customer. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n",
+ " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n",
+ " \"\\nCurrent time: {time}.\"\n",
+ " '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant.'\n",
+ " \" Do not waste the user's time. Do not make up invalid tools or functions.\"\n",
+ " \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n",
+ " \" - 'what's the weather like this time of year?'\\n\"\n",
+ " \" - 'nevermind i think I'll book separately'\\n\"\n",
+ " \" - 'i need to figure out transportation while i'm there'\\n\"\n",
+ " \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n",
+ " \" - 'Hotel booking confirmed'\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "book_hotel_safe_tools = [search_hotels]\n",
+ "book_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]\n",
+ "book_hotel_tools = book_hotel_safe_tools + book_hotel_sensitive_tools\n",
+ "book_hotel_runnable = book_hotel_prompt | llm.bind_tools(\n",
+ " book_hotel_tools + [CompleteOrEscalate]\n",
+ ")\n",
+ "\n",
+ "# Car Rental Assistant\n",
+ "book_car_rental_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a specialized assistant for handling car rental bookings. \"\n",
+ " \"The primary assistant delegates work to you whenever the user needs help booking a car rental. \"\n",
+ " \"Search for available car rentals based on the user's preferences and confirm the booking details with the customer. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n",
+ " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n",
+ " \"\\nCurrent time: {time}.\"\n",
+ " \"\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"\n",
+ " '\"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n",
+ " \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n",
+ " \" - 'what's the weather like this time of year?'\\n\"\n",
+ " \" - 'What flights are available?'\\n\"\n",
+ " \" - 'nevermind i think I'll book separately'\\n\"\n",
+ " \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n",
+ " \" - 'Car rental booking confirmed'\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "book_car_rental_safe_tools = [search_car_rentals]\n",
+ "book_car_rental_sensitive_tools = [\n",
+ " book_car_rental,\n",
+ " update_car_rental,\n",
+ " cancel_car_rental,\n",
+ "]\n",
+ "book_car_rental_tools = book_car_rental_safe_tools + book_car_rental_sensitive_tools\n",
+ "book_car_rental_runnable = book_car_rental_prompt | llm.bind_tools(\n",
+ " book_car_rental_tools + [CompleteOrEscalate]\n",
+ ")\n",
+ "\n",
+ "# Excursion Assistant\n",
+ "\n",
+ "book_excursion_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a specialized assistant for handling trip recommendations. \"\n",
+ " \"The primary assistant delegates work to you whenever the user needs help booking a recommended trip. \"\n",
+ " \"Search for available trip recommendations based on the user's preferences and confirm the booking details with the customer. \"\n",
+ " \"If you need more information or the customer changes their mind, escalate the task back to the main assistant.\"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \" Remember that a booking isn't completed until after the relevant tool has successfully been used.\"\n",
+ " \"\\nCurrent time: {time}.\"\n",
+ " '\\n\\nIf the user needs help, and none of your tools are appropriate for it, then \"CompleteOrEscalate\" the dialog to the host assistant. Do not waste the user\\'s time. Do not make up invalid tools or functions.'\n",
+ " \"\\n\\nSome examples for which you should CompleteOrEscalate:\\n\"\n",
+ " \" - 'nevermind i think I'll book separately'\\n\"\n",
+ " \" - 'i need to figure out transportation while i'm there'\\n\"\n",
+ " \" - 'Oh wait i haven't booked my flight yet i'll do that first'\\n\"\n",
+ " \" - 'Excursion booking confirmed!'\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "\n",
+ "book_excursion_safe_tools = [search_trip_recommendations]\n",
+ "book_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]\n",
+ "book_excursion_tools = book_excursion_safe_tools + book_excursion_sensitive_tools\n",
+ "book_excursion_runnable = book_excursion_prompt | llm.bind_tools(\n",
+ " book_excursion_tools + [CompleteOrEscalate]\n",
+ ")\n",
+ "\n",
+ "\n",
+ "# Primary Assistant\n",
+ "class ToFlightBookingAssistant(BaseModel):\n",
+ " \"\"\"Transfers work to a specialized assistant to handle flight updates and cancellations.\"\"\"\n",
+ "\n",
+ " request: str = Field(\n",
+ " description=\"Any necessary followup questions the update flight assistant should clarify before proceeding.\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "class ToBookCarRental(BaseModel):\n",
+ " \"\"\"Transfers work to a specialized assistant to handle car rental bookings.\"\"\"\n",
+ "\n",
+ " location: str = Field(\n",
+ " description=\"The location where the user wants to rent a car.\"\n",
+ " )\n",
+ " start_date: str = Field(description=\"The start date of the car rental.\")\n",
+ " end_date: str = Field(description=\"The end date of the car rental.\")\n",
+ " request: str = Field(\n",
+ " description=\"Any additional information or requests from the user regarding the car rental.\"\n",
+ " )\n",
+ "\n",
+ " class Config:\n",
+ " schema_extra = {\n",
+ " \"example\": {\n",
+ " \"location\": \"Basel\",\n",
+ " \"start_date\": \"2023-07-01\",\n",
+ " \"end_date\": \"2023-07-05\",\n",
+ " \"request\": \"I need a compact car with automatic transmission.\",\n",
+ " }\n",
+ " }\n",
+ "\n",
+ "\n",
+ "class ToHotelBookingAssistant(BaseModel):\n",
+ " \"\"\"Transfer work to a specialized assistant to handle hotel bookings.\"\"\"\n",
+ "\n",
+ " location: str = Field(\n",
+ " description=\"The location where the user wants to book a hotel.\"\n",
+ " )\n",
+ " checkin_date: str = Field(description=\"The check-in date for the hotel.\")\n",
+ " checkout_date: str = Field(description=\"The check-out date for the hotel.\")\n",
+ " request: str = Field(\n",
+ " description=\"Any additional information or requests from the user regarding the hotel booking.\"\n",
+ " )\n",
+ "\n",
+ " class Config:\n",
+ " schema_extra = {\n",
+ " \"example\": {\n",
+ " \"location\": \"Zurich\",\n",
+ " \"checkin_date\": \"2023-08-15\",\n",
+ " \"checkout_date\": \"2023-08-20\",\n",
+ " \"request\": \"I prefer a hotel near the city center with a room that has a view.\",\n",
+ " }\n",
+ " }\n",
+ "\n",
+ "\n",
+ "class ToBookExcursion(BaseModel):\n",
+ " \"\"\"Transfers work to a specialized assistant to handle trip recommendation and other excursion bookings.\"\"\"\n",
+ "\n",
+ " location: str = Field(\n",
+ " description=\"The location where the user wants to book a recommended trip.\"\n",
+ " )\n",
+ " request: str = Field(\n",
+ " description=\"Any additional information or requests from the user regarding the trip recommendation.\"\n",
+ " )\n",
+ "\n",
+ " class Config:\n",
+ " schema_extra = {\n",
+ " \"example\": {\n",
+ " \"location\": \"Lucerne\",\n",
+ " \"request\": \"The user is interested in outdoor activities and scenic views.\",\n",
+ " }\n",
+ " }\n",
+ "\n",
+ "\n",
+ "# The top-level assistant performs general Q&A and delegates specialized tasks to other assistants.\n",
+ "# The task delegation is a simple form of semantic routing / does simple intent detection\n",
+ "# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\", temperature=1)\n",
+ "\n",
+ "primary_assistant_prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\n",
+ " \"system\",\n",
+ " \"You are a helpful customer support assistant for Swiss Airlines. \"\n",
+ " \"Your primary role is to search for flight information and company policies to answer customer queries. \"\n",
+ " \"If a customer requests to update or cancel a flight, book a car rental, book a hotel, or get trip recommendations, \"\n",
+ " \"delegate the task to the appropriate specialized assistant by invoking the corresponding tool. You are not able to make these types of changes yourself.\"\n",
+ " \" Only the specialized assistants are given permission to do this for the user.\"\n",
+ " \"The user is not aware of the different specialized assistants, so do not mention them; just quietly delegate through function calls. \"\n",
+ " \"Provide detailed information to the customer, and always double-check the database before concluding that information is unavailable. \"\n",
+ " \" When searching, be persistent. Expand your query bounds if the first search returns no results. \"\n",
+ " \" If a search comes up empty, expand your search before giving up.\"\n",
+ " \"\\n\\nCurrent user flight information:\\n\\n{user_info}\\n\"\n",
+ " \"\\nCurrent time: {time}.\",\n",
+ " ),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ").partial(time=datetime.now())\n",
+ "primary_assistant_tools = [\n",
+ " TavilySearchResults(max_results=1),\n",
+ " search_flights,\n",
+ " lookup_policy,\n",
+ "]\n",
+ "assistant_runnable = primary_assistant_prompt | llm.bind_tools(\n",
+ " primary_assistant_tools\n",
+ " + [\n",
+ " ToFlightBookingAssistant,\n",
+ " ToBookCarRental,\n",
+ " ToHotelBookingAssistant,\n",
+ " ToBookExcursion,\n",
+ " ]\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -1725,7 +3396,31 @@
"id": "fb812818-99c9-4bf3-b1e5-a394c7b9058d",
"metadata": {},
"outputs": [],
- "source": ["from typing import Callable\n\nfrom langchain_core.messages import ToolMessage\n\n\ndef create_entry_node(assistant_name: str, new_dialog_state: str) -> Callable:\n def entry_node(state: State) -> dict:\n tool_call_id = state[\"messages\"][-1].tool_calls[0][\"id\"]\n return {\n \"messages\": [\n ToolMessage(\n content=f\"The assistant is now the {assistant_name}. Reflect on the above conversation between the host assistant and the user.\"\n f\" The user's intent is unsatisfied. Use the provided tools to assist the user. Remember, you are {assistant_name},\"\n \" and the booking, update, other other action is not complete until after you have successfully invoked the appropriate tool.\"\n \" If the user changes their mind or needs help for other tasks, call the CompleteOrEscalate function to let the primary host assistant take control.\"\n \" Do not mention who you are - just act as the proxy for the assistant.\",\n tool_call_id=tool_call_id,\n )\n ],\n \"dialog_state\": new_dialog_state,\n }\n\n return entry_node"]
+ "source": [
+ "from typing import Callable\n",
+ "\n",
+ "from langchain_core.messages import ToolMessage\n",
+ "\n",
+ "\n",
+ "def create_entry_node(assistant_name: str, new_dialog_state: str) -> Callable:\n",
+ " def entry_node(state: State) -> dict:\n",
+ " tool_call_id = state[\"messages\"][-1].tool_calls[0][\"id\"]\n",
+ " return {\n",
+ " \"messages\": [\n",
+ " ToolMessage(\n",
+ " content=f\"The assistant is now the {assistant_name}. Reflect on the above conversation between the host assistant and the user.\"\n",
+ " f\" The user's intent is unsatisfied. Use the provided tools to assist the user. Remember, you are {assistant_name},\"\n",
+ " \" and the booking, update, other other action is not complete until after you have successfully invoked the appropriate tool.\"\n",
+ " \" If the user changes their mind or needs help for other tasks, call the CompleteOrEscalate function to let the primary host assistant take control.\"\n",
+ " \" Do not mention who you are - just act as the proxy for the assistant.\",\n",
+ " tool_call_id=tool_call_id,\n",
+ " )\n",
+ " ],\n",
+ " \"dialog_state\": new_dialog_state,\n",
+ " }\n",
+ "\n",
+ " return entry_node"
+ ]
},
{
"cell_type": "markdown",
@@ -1743,7 +3438,23 @@
"id": "b7c1140c-cd4e-4d69-bddd-7baa1eb4540e",
"metadata": {},
"outputs": [],
- "source": ["from typing import Literal\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph\nfrom langgraph.prebuilt import tools_condition\n\nbuilder = StateGraph(State)\n\n\ndef user_info(state: State):\n return {\"user_info\": fetch_user_flight_information.invoke({})}\n\n\nbuilder.add_node(\"fetch_user_info\", user_info)\nbuilder.add_edge(START, \"fetch_user_info\")"]
+ "source": [
+ "from typing import Literal\n",
+ "\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "from langgraph.graph import StateGraph\n",
+ "from langgraph.prebuilt import tools_condition\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "def user_info(state: State):\n",
+ " return {\"user_info\": fetch_user_flight_information.invoke({})}\n",
+ "\n",
+ "\n",
+ "builder.add_node(\"fetch_user_info\", user_info)\n",
+ "builder.add_edge(START, \"fetch_user_info\")"
+ ]
},
{
"cell_type": "markdown",
@@ -1769,7 +3480,75 @@
"id": "54297dc5-80b2-4bc6-8087-803caf1e0cf7",
"metadata": {},
"outputs": [],
- "source": ["# Flight booking assistant\nbuilder.add_node(\n \"enter_update_flight\",\n create_entry_node(\"Flight Updates & Booking Assistant\", \"update_flight\"),\n)\nbuilder.add_node(\"update_flight\", Assistant(update_flight_runnable))\nbuilder.add_edge(\"enter_update_flight\", \"update_flight\")\nbuilder.add_node(\n \"update_flight_sensitive_tools\",\n create_tool_node_with_fallback(update_flight_sensitive_tools),\n)\nbuilder.add_node(\n \"update_flight_safe_tools\",\n create_tool_node_with_fallback(update_flight_safe_tools),\n)\n\n\ndef route_update_flight(\n state: State,\n) -> Literal[\n \"update_flight_sensitive_tools\",\n \"update_flight_safe_tools\",\n \"leave_skill\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n safe_toolnames = [t.name for t in update_flight_safe_tools]\n if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n return \"update_flight_safe_tools\"\n return \"update_flight_sensitive_tools\"\n\n\nbuilder.add_edge(\"update_flight_sensitive_tools\", \"update_flight\")\nbuilder.add_edge(\"update_flight_safe_tools\", \"update_flight\")\nbuilder.add_conditional_edges(\"update_flight\", route_update_flight)\n\n\n# This node will be shared for exiting all specialized assistants\ndef pop_dialog_state(state: State) -> dict:\n \"\"\"Pop the dialog stack and return to the main assistant.\n\n This lets the full graph explicitly track the dialog flow and delegate control\n to specific sub-graphs.\n \"\"\"\n messages = []\n if state[\"messages\"][-1].tool_calls:\n # Note: Doesn't currently handle the edge case where the llm performs parallel tool calls\n messages.append(\n ToolMessage(\n content=\"Resuming dialog with the host assistant. Please reflect on the past conversation and assist the user as needed.\",\n tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n )\n )\n return {\n \"dialog_state\": \"pop\",\n \"messages\": messages,\n }\n\n\nbuilder.add_node(\"leave_skill\", pop_dialog_state)\nbuilder.add_edge(\"leave_skill\", \"primary_assistant\")"]
+ "source": [
+ "# Flight booking assistant\n",
+ "builder.add_node(\n",
+ " \"enter_update_flight\",\n",
+ " create_entry_node(\"Flight Updates & Booking Assistant\", \"update_flight\"),\n",
+ ")\n",
+ "builder.add_node(\"update_flight\", Assistant(update_flight_runnable))\n",
+ "builder.add_edge(\"enter_update_flight\", \"update_flight\")\n",
+ "builder.add_node(\n",
+ " \"update_flight_sensitive_tools\",\n",
+ " create_tool_node_with_fallback(update_flight_sensitive_tools),\n",
+ ")\n",
+ "builder.add_node(\n",
+ " \"update_flight_safe_tools\",\n",
+ " create_tool_node_with_fallback(update_flight_safe_tools),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def route_update_flight(\n",
+ " state: State,\n",
+ ") -> Literal[\n",
+ " \"update_flight_sensitive_tools\",\n",
+ " \"update_flight_safe_tools\",\n",
+ " \"leave_skill\",\n",
+ " \"__end__\",\n",
+ "]:\n",
+ " route = tools_condition(state)\n",
+ " if route == END:\n",
+ " return END\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n",
+ " if did_cancel:\n",
+ " return \"leave_skill\"\n",
+ " safe_toolnames = [t.name for t in update_flight_safe_tools]\n",
+ " if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n",
+ " return \"update_flight_safe_tools\"\n",
+ " return \"update_flight_sensitive_tools\"\n",
+ "\n",
+ "\n",
+ "builder.add_edge(\"update_flight_sensitive_tools\", \"update_flight\")\n",
+ "builder.add_edge(\"update_flight_safe_tools\", \"update_flight\")\n",
+ "builder.add_conditional_edges(\"update_flight\", route_update_flight)\n",
+ "\n",
+ "\n",
+ "# This node will be shared for exiting all specialized assistants\n",
+ "def pop_dialog_state(state: State) -> dict:\n",
+ " \"\"\"Pop the dialog stack and return to the main assistant.\n",
+ "\n",
+ " This lets the full graph explicitly track the dialog flow and delegate control\n",
+ " to specific sub-graphs.\n",
+ " \"\"\"\n",
+ " messages = []\n",
+ " if state[\"messages\"][-1].tool_calls:\n",
+ " # Note: Doesn't currently handle the edge case where the llm performs parallel tool calls\n",
+ " messages.append(\n",
+ " ToolMessage(\n",
+ " content=\"Resuming dialog with the host assistant. Please reflect on the past conversation and assist the user as needed.\",\n",
+ " tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n",
+ " )\n",
+ " )\n",
+ " return {\n",
+ " \"dialog_state\": \"pop\",\n",
+ " \"messages\": messages,\n",
+ " }\n",
+ "\n",
+ "\n",
+ "builder.add_node(\"leave_skill\", pop_dialog_state)\n",
+ "builder.add_edge(\"leave_skill\", \"primary_assistant\")"
+ ]
},
{
"cell_type": "markdown",
@@ -1785,7 +3564,50 @@
"id": "e68b93f5-0f72-4e94-8e8b-b501ec82edcf",
"metadata": {},
"outputs": [],
- "source": ["# Car rental assistant\n\nbuilder.add_node(\n \"enter_book_car_rental\",\n create_entry_node(\"Car Rental Assistant\", \"book_car_rental\"),\n)\nbuilder.add_node(\"book_car_rental\", Assistant(book_car_rental_runnable))\nbuilder.add_edge(\"enter_book_car_rental\", \"book_car_rental\")\nbuilder.add_node(\n \"book_car_rental_safe_tools\",\n create_tool_node_with_fallback(book_car_rental_safe_tools),\n)\nbuilder.add_node(\n \"book_car_rental_sensitive_tools\",\n create_tool_node_with_fallback(book_car_rental_sensitive_tools),\n)\n\n\ndef route_book_car_rental(\n state: State,\n) -> Literal[\n \"book_car_rental_safe_tools\",\n \"book_car_rental_sensitive_tools\",\n \"leave_skill\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n safe_toolnames = [t.name for t in book_car_rental_safe_tools]\n if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n return \"book_car_rental_safe_tools\"\n return \"book_car_rental_sensitive_tools\"\n\n\nbuilder.add_edge(\"book_car_rental_sensitive_tools\", \"book_car_rental\")\nbuilder.add_edge(\"book_car_rental_safe_tools\", \"book_car_rental\")\nbuilder.add_conditional_edges(\"book_car_rental\", route_book_car_rental)"]
+ "source": [
+ "# Car rental assistant\n",
+ "\n",
+ "builder.add_node(\n",
+ " \"enter_book_car_rental\",\n",
+ " create_entry_node(\"Car Rental Assistant\", \"book_car_rental\"),\n",
+ ")\n",
+ "builder.add_node(\"book_car_rental\", Assistant(book_car_rental_runnable))\n",
+ "builder.add_edge(\"enter_book_car_rental\", \"book_car_rental\")\n",
+ "builder.add_node(\n",
+ " \"book_car_rental_safe_tools\",\n",
+ " create_tool_node_with_fallback(book_car_rental_safe_tools),\n",
+ ")\n",
+ "builder.add_node(\n",
+ " \"book_car_rental_sensitive_tools\",\n",
+ " create_tool_node_with_fallback(book_car_rental_sensitive_tools),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def route_book_car_rental(\n",
+ " state: State,\n",
+ ") -> Literal[\n",
+ " \"book_car_rental_safe_tools\",\n",
+ " \"book_car_rental_sensitive_tools\",\n",
+ " \"leave_skill\",\n",
+ " \"__end__\",\n",
+ "]:\n",
+ " route = tools_condition(state)\n",
+ " if route == END:\n",
+ " return END\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n",
+ " if did_cancel:\n",
+ " return \"leave_skill\"\n",
+ " safe_toolnames = [t.name for t in book_car_rental_safe_tools]\n",
+ " if all(tc[\"name\"] in safe_toolnames for tc in tool_calls):\n",
+ " return \"book_car_rental_safe_tools\"\n",
+ " return \"book_car_rental_sensitive_tools\"\n",
+ "\n",
+ "\n",
+ "builder.add_edge(\"book_car_rental_sensitive_tools\", \"book_car_rental\")\n",
+ "builder.add_edge(\"book_car_rental_safe_tools\", \"book_car_rental\")\n",
+ "builder.add_conditional_edges(\"book_car_rental\", route_book_car_rental)"
+ ]
},
{
"cell_type": "markdown",
@@ -1801,7 +3623,45 @@
"id": "ec40edb9-d415-4f43-8f9f-c82a239c607f",
"metadata": {},
"outputs": [],
- "source": ["# Hotel booking assistant\nbuilder.add_node(\n \"enter_book_hotel\", create_entry_node(\"Hotel Booking Assistant\", \"book_hotel\")\n)\nbuilder.add_node(\"book_hotel\", Assistant(book_hotel_runnable))\nbuilder.add_edge(\"enter_book_hotel\", \"book_hotel\")\nbuilder.add_node(\n \"book_hotel_safe_tools\",\n create_tool_node_with_fallback(book_hotel_safe_tools),\n)\nbuilder.add_node(\n \"book_hotel_sensitive_tools\",\n create_tool_node_with_fallback(book_hotel_sensitive_tools),\n)\n\n\ndef route_book_hotel(\n state: State,\n) -> Literal[\n \"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", \"__end__\"\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n tool_names = [t.name for t in book_hotel_safe_tools]\n if all(tc[\"name\"] in tool_names for tc in tool_calls):\n return \"book_hotel_safe_tools\"\n return \"book_hotel_sensitive_tools\"\n\n\nbuilder.add_edge(\"book_hotel_sensitive_tools\", \"book_hotel\")\nbuilder.add_edge(\"book_hotel_safe_tools\", \"book_hotel\")\nbuilder.add_conditional_edges(\"book_hotel\", route_book_hotel)"]
+ "source": [
+ "# Hotel booking assistant\n",
+ "builder.add_node(\n",
+ " \"enter_book_hotel\", create_entry_node(\"Hotel Booking Assistant\", \"book_hotel\")\n",
+ ")\n",
+ "builder.add_node(\"book_hotel\", Assistant(book_hotel_runnable))\n",
+ "builder.add_edge(\"enter_book_hotel\", \"book_hotel\")\n",
+ "builder.add_node(\n",
+ " \"book_hotel_safe_tools\",\n",
+ " create_tool_node_with_fallback(book_hotel_safe_tools),\n",
+ ")\n",
+ "builder.add_node(\n",
+ " \"book_hotel_sensitive_tools\",\n",
+ " create_tool_node_with_fallback(book_hotel_sensitive_tools),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def route_book_hotel(\n",
+ " state: State,\n",
+ ") -> Literal[\n",
+ " \"leave_skill\", \"book_hotel_safe_tools\", \"book_hotel_sensitive_tools\", \"__end__\"\n",
+ "]:\n",
+ " route = tools_condition(state)\n",
+ " if route == END:\n",
+ " return END\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n",
+ " if did_cancel:\n",
+ " return \"leave_skill\"\n",
+ " tool_names = [t.name for t in book_hotel_safe_tools]\n",
+ " if all(tc[\"name\"] in tool_names for tc in tool_calls):\n",
+ " return \"book_hotel_safe_tools\"\n",
+ " return \"book_hotel_sensitive_tools\"\n",
+ "\n",
+ "\n",
+ "builder.add_edge(\"book_hotel_sensitive_tools\", \"book_hotel\")\n",
+ "builder.add_edge(\"book_hotel_safe_tools\", \"book_hotel\")\n",
+ "builder.add_conditional_edges(\"book_hotel\", route_book_hotel)"
+ ]
},
{
"cell_type": "markdown",
@@ -1817,7 +3677,49 @@
"id": "2ce9cf21-f708-4033-bca6-5f5d110b5662",
"metadata": {},
"outputs": [],
- "source": ["# Excursion assistant\nbuilder.add_node(\n \"enter_book_excursion\",\n create_entry_node(\"Trip Recommendation Assistant\", \"book_excursion\"),\n)\nbuilder.add_node(\"book_excursion\", Assistant(book_excursion_runnable))\nbuilder.add_edge(\"enter_book_excursion\", \"book_excursion\")\nbuilder.add_node(\n \"book_excursion_safe_tools\",\n create_tool_node_with_fallback(book_excursion_safe_tools),\n)\nbuilder.add_node(\n \"book_excursion_sensitive_tools\",\n create_tool_node_with_fallback(book_excursion_sensitive_tools),\n)\n\n\ndef route_book_excursion(\n state: State,\n) -> Literal[\n \"book_excursion_safe_tools\",\n \"book_excursion_sensitive_tools\",\n \"leave_skill\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n if did_cancel:\n return \"leave_skill\"\n tool_names = [t.name for t in book_excursion_safe_tools]\n if all(tc[\"name\"] in tool_names for tc in tool_calls):\n return \"book_excursion_safe_tools\"\n return \"book_excursion_sensitive_tools\"\n\n\nbuilder.add_edge(\"book_excursion_sensitive_tools\", \"book_excursion\")\nbuilder.add_edge(\"book_excursion_safe_tools\", \"book_excursion\")\nbuilder.add_conditional_edges(\"book_excursion\", route_book_excursion)"]
+ "source": [
+ "# Excursion assistant\n",
+ "builder.add_node(\n",
+ " \"enter_book_excursion\",\n",
+ " create_entry_node(\"Trip Recommendation Assistant\", \"book_excursion\"),\n",
+ ")\n",
+ "builder.add_node(\"book_excursion\", Assistant(book_excursion_runnable))\n",
+ "builder.add_edge(\"enter_book_excursion\", \"book_excursion\")\n",
+ "builder.add_node(\n",
+ " \"book_excursion_safe_tools\",\n",
+ " create_tool_node_with_fallback(book_excursion_safe_tools),\n",
+ ")\n",
+ "builder.add_node(\n",
+ " \"book_excursion_sensitive_tools\",\n",
+ " create_tool_node_with_fallback(book_excursion_sensitive_tools),\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def route_book_excursion(\n",
+ " state: State,\n",
+ ") -> Literal[\n",
+ " \"book_excursion_safe_tools\",\n",
+ " \"book_excursion_sensitive_tools\",\n",
+ " \"leave_skill\",\n",
+ " \"__end__\",\n",
+ "]:\n",
+ " route = tools_condition(state)\n",
+ " if route == END:\n",
+ " return END\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " did_cancel = any(tc[\"name\"] == CompleteOrEscalate.__name__ for tc in tool_calls)\n",
+ " if did_cancel:\n",
+ " return \"leave_skill\"\n",
+ " tool_names = [t.name for t in book_excursion_safe_tools]\n",
+ " if all(tc[\"name\"] in tool_names for tc in tool_calls):\n",
+ " return \"book_excursion_safe_tools\"\n",
+ " return \"book_excursion_sensitive_tools\"\n",
+ "\n",
+ "\n",
+ "builder.add_edge(\"book_excursion_sensitive_tools\", \"book_excursion\")\n",
+ "builder.add_edge(\"book_excursion_safe_tools\", \"book_excursion\")\n",
+ "builder.add_conditional_edges(\"book_excursion\", route_book_excursion)"
+ ]
},
{
"cell_type": "markdown",
@@ -1833,7 +3735,90 @@
"id": "acb19faf-66c8-4fd8-89ec-4d97d510ce4d",
"metadata": {},
"outputs": [],
- "source": ["# Primary assistant\nbuilder.add_node(\"primary_assistant\", Assistant(assistant_runnable))\nbuilder.add_node(\n \"primary_assistant_tools\", create_tool_node_with_fallback(primary_assistant_tools)\n)\n\n\ndef route_primary_assistant(\n state: State,\n) -> Literal[\n \"primary_assistant_tools\",\n \"enter_update_flight\",\n \"enter_book_hotel\",\n \"enter_book_excursion\",\n \"__end__\",\n]:\n route = tools_condition(state)\n if route == END:\n return END\n tool_calls = state[\"messages\"][-1].tool_calls\n if tool_calls:\n if tool_calls[0][\"name\"] == ToFlightBookingAssistant.__name__:\n return \"enter_update_flight\"\n elif tool_calls[0][\"name\"] == ToBookCarRental.__name__:\n return \"enter_book_car_rental\"\n elif tool_calls[0][\"name\"] == ToHotelBookingAssistant.__name__:\n return \"enter_book_hotel\"\n elif tool_calls[0][\"name\"] == ToBookExcursion.__name__:\n return \"enter_book_excursion\"\n return \"primary_assistant_tools\"\n raise ValueError(\"Invalid route\")\n\n\n# The assistant can route to one of the delegated assistants,\n# directly use a tool, or directly respond to the user\nbuilder.add_conditional_edges(\n \"primary_assistant\",\n route_primary_assistant,\n {\n \"enter_update_flight\": \"enter_update_flight\",\n \"enter_book_car_rental\": \"enter_book_car_rental\",\n \"enter_book_hotel\": \"enter_book_hotel\",\n \"enter_book_excursion\": \"enter_book_excursion\",\n \"primary_assistant_tools\": \"primary_assistant_tools\",\n END: END,\n },\n)\nbuilder.add_edge(\"primary_assistant_tools\", \"primary_assistant\")\n\n\n# Each delegated workflow can directly respond to the user\n# When the user responds, we want to return to the currently active workflow\ndef route_to_workflow(\n state: State,\n) -> Literal[\n \"primary_assistant\",\n \"update_flight\",\n \"book_car_rental\",\n \"book_hotel\",\n \"book_excursion\",\n]:\n \"\"\"If we are in a delegated state, route directly to the appropriate assistant.\"\"\"\n dialog_state = state.get(\"dialog_state\")\n if not dialog_state:\n return \"primary_assistant\"\n return dialog_state[-1]\n\n\nbuilder.add_conditional_edges(\"fetch_user_info\", route_to_workflow)\n\n# Compile graph\nmemory = SqliteSaver.from_conn_string(\":memory:\")\npart_4_graph = builder.compile(\n checkpointer=memory,\n # Let the user approve or deny the use of sensitive tools\n interrupt_before=[\n \"update_flight_sensitive_tools\",\n \"book_car_rental_sensitive_tools\",\n \"book_hotel_sensitive_tools\",\n \"book_excursion_sensitive_tools\",\n ],\n)"]
+ "source": [
+ "# Primary assistant\n",
+ "builder.add_node(\"primary_assistant\", Assistant(assistant_runnable))\n",
+ "builder.add_node(\n",
+ " \"primary_assistant_tools\", create_tool_node_with_fallback(primary_assistant_tools)\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def route_primary_assistant(\n",
+ " state: State,\n",
+ ") -> Literal[\n",
+ " \"primary_assistant_tools\",\n",
+ " \"enter_update_flight\",\n",
+ " \"enter_book_hotel\",\n",
+ " \"enter_book_excursion\",\n",
+ " \"__end__\",\n",
+ "]:\n",
+ " route = tools_condition(state)\n",
+ " if route == END:\n",
+ " return END\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " if tool_calls:\n",
+ " if tool_calls[0][\"name\"] == ToFlightBookingAssistant.__name__:\n",
+ " return \"enter_update_flight\"\n",
+ " elif tool_calls[0][\"name\"] == ToBookCarRental.__name__:\n",
+ " return \"enter_book_car_rental\"\n",
+ " elif tool_calls[0][\"name\"] == ToHotelBookingAssistant.__name__:\n",
+ " return \"enter_book_hotel\"\n",
+ " elif tool_calls[0][\"name\"] == ToBookExcursion.__name__:\n",
+ " return \"enter_book_excursion\"\n",
+ " return \"primary_assistant_tools\"\n",
+ " raise ValueError(\"Invalid route\")\n",
+ "\n",
+ "\n",
+ "# The assistant can route to one of the delegated assistants,\n",
+ "# directly use a tool, or directly respond to the user\n",
+ "builder.add_conditional_edges(\n",
+ " \"primary_assistant\",\n",
+ " route_primary_assistant,\n",
+ " {\n",
+ " \"enter_update_flight\": \"enter_update_flight\",\n",
+ " \"enter_book_car_rental\": \"enter_book_car_rental\",\n",
+ " \"enter_book_hotel\": \"enter_book_hotel\",\n",
+ " \"enter_book_excursion\": \"enter_book_excursion\",\n",
+ " \"primary_assistant_tools\": \"primary_assistant_tools\",\n",
+ " END: END,\n",
+ " },\n",
+ ")\n",
+ "builder.add_edge(\"primary_assistant_tools\", \"primary_assistant\")\n",
+ "\n",
+ "\n",
+ "# Each delegated workflow can directly respond to the user\n",
+ "# When the user responds, we want to return to the currently active workflow\n",
+ "def route_to_workflow(\n",
+ " state: State,\n",
+ ") -> Literal[\n",
+ " \"primary_assistant\",\n",
+ " \"update_flight\",\n",
+ " \"book_car_rental\",\n",
+ " \"book_hotel\",\n",
+ " \"book_excursion\",\n",
+ "]:\n",
+ " \"\"\"If we are in a delegated state, route directly to the appropriate assistant.\"\"\"\n",
+ " dialog_state = state.get(\"dialog_state\")\n",
+ " if not dialog_state:\n",
+ " return \"primary_assistant\"\n",
+ " return dialog_state[-1]\n",
+ "\n",
+ "\n",
+ "builder.add_conditional_edges(\"fetch_user_info\", route_to_workflow)\n",
+ "\n",
+ "# Compile graph\n",
+ "memory = MemorySaver()\n",
+ "part_4_graph = builder.compile(\n",
+ " checkpointer=memory,\n",
+ " # Let the user approve or deny the use of sensitive tools\n",
+ " interrupt_before=[\n",
+ " \"update_flight_sensitive_tools\",\n",
+ " \"book_car_rental_sensitive_tools\",\n",
+ " \"book_hotel_sensitive_tools\",\n",
+ " \"book_excursion_sensitive_tools\",\n",
+ " ],\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -1852,7 +3837,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(part_4_graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(part_4_graph.get_graph(xray=True).draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -2326,7 +4319,63 @@
]
}
],
- "source": ["import shutil\nimport uuid\n\n# Update with the backup file so we can restart from the original place in each section\nshutil.copy(backup_file, db)\nthread_id = str(uuid.uuid4())\n\nconfig = {\n \"configurable\": {\n # The passenger_id is used in our flight tools to\n # fetch the user's flight information\n \"passenger_id\": \"3442 587242\",\n # Checkpoints are accessed by thread_id\n \"thread_id\": thread_id,\n }\n}\n\n_printed = set()\n# We can reuse the tutorial questions from part 1 to see how it does.\nfor question in tutorial_questions:\n events = part_4_graph.stream(\n {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n )\n for event in events:\n _print_event(event, _printed)\n snapshot = part_4_graph.get_state(config)\n while snapshot.next:\n # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n user_input = input(\n \"Do you approve of the above actions? Type 'y' to continue;\"\n \" otherwise, explain your requested changed.\\n\\n\"\n )\n if user_input.strip() == \"y\":\n # Just continue\n result = part_4_graph.invoke(\n None,\n config,\n )\n else:\n # Satisfy the tool invocation by\n # providing instructions on the requested changes / change of mind\n result = part_4_graph.invoke(\n {\n \"messages\": [\n ToolMessage(\n tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n )\n ]\n },\n config,\n )\n snapshot = part_4_graph.get_state(config)"]
+ "source": [
+ "import shutil\n",
+ "import uuid\n",
+ "\n",
+ "# Update with the backup file so we can restart from the original place in each section\n",
+ "shutil.copy(backup_file, db)\n",
+ "thread_id = str(uuid.uuid4())\n",
+ "\n",
+ "config = {\n",
+ " \"configurable\": {\n",
+ " # The passenger_id is used in our flight tools to\n",
+ " # fetch the user's flight information\n",
+ " \"passenger_id\": \"3442 587242\",\n",
+ " # Checkpoints are accessed by thread_id\n",
+ " \"thread_id\": thread_id,\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "_printed = set()\n",
+ "# We can reuse the tutorial questions from part 1 to see how it does.\n",
+ "for question in tutorial_questions:\n",
+ " events = part_4_graph.stream(\n",
+ " {\"messages\": (\"user\", question)}, config, stream_mode=\"values\"\n",
+ " )\n",
+ " for event in events:\n",
+ " _print_event(event, _printed)\n",
+ " snapshot = part_4_graph.get_state(config)\n",
+ " while snapshot.next:\n",
+ " # We have an interrupt! The agent is trying to use a tool, and the user can approve or deny it\n",
+ " # Note: This code is all outside of your graph. Typically, you would stream the output to a UI.\n",
+ " # Then, you would have the frontend trigger a new run via an API call when the user has provided input.\n",
+ " user_input = input(\n",
+ " \"Do you approve of the above actions? Type 'y' to continue;\"\n",
+ " \" otherwise, explain your requested changed.\\n\\n\"\n",
+ " )\n",
+ " if user_input.strip() == \"y\":\n",
+ " # Just continue\n",
+ " result = part_4_graph.invoke(\n",
+ " None,\n",
+ " config,\n",
+ " )\n",
+ " else:\n",
+ " # Satisfy the tool invocation by\n",
+ " # providing instructions on the requested changes / change of mind\n",
+ " result = part_4_graph.invoke(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " ToolMessage(\n",
+ " tool_call_id=event[\"messages\"][-1].tool_calls[0][\"id\"],\n",
+ " content=f\"API call denied by user. Reasoning: '{user_input}'. Continue assisting, accounting for the user's input.\",\n",
+ " )\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " )\n",
+ " snapshot = part_4_graph.get_state(config)"
+ ]
},
{
"cell_type": "markdown",
diff --git a/examples/human-in-the-loop.ipynb b/examples/human-in-the-loop.ipynb
index 0dd453a4c..8dbc0b5d5 100644
--- a/examples/human-in-the-loop.ipynb
+++ b/examples/human-in-the-loop.ipynb
@@ -39,7 +39,10 @@
"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",
@@ -55,7 +58,18 @@
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"metadata": {},
"outputs": [],
- "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\")"]
+ "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\")"
+ ]
},
{
"cell_type": "markdown",
@@ -71,7 +85,10 @@
"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",
@@ -89,7 +106,22 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
- "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]"]
+ "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]"
+ ]
},
{
"cell_type": "markdown",
@@ -109,7 +141,22 @@
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
"metadata": {},
"outputs": [],
- "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]"]
+ "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]"
+ ]
},
{
"cell_type": "markdown",
@@ -127,7 +174,11 @@
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.prebuilt import ToolExecutor\n\ntool_executor = ToolExecutor(tools)"]
+ "source": [
+ "from langgraph.prebuilt import ToolExecutor\n",
+ "\n",
+ "tool_executor = ToolExecutor(tools)"
+ ]
},
{
"cell_type": "markdown",
@@ -148,7 +199,11 @@
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
"metadata": {},
"outputs": [],
- "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
+ "source": [
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "model = ChatOpenAI(temperature=0)"
+ ]
},
{
"cell_type": "markdown",
@@ -166,7 +221,9 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
- "source": ["model = model.bind_tools(tools)"]
+ "source": [
+ "model = model.bind_tools(tools)"
+ ]
},
{
"cell_type": "markdown",
@@ -201,7 +258,53 @@
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
"metadata": {},
"outputs": [],
- "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]}"]
+ "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]}"
+ ]
},
{
"cell_type": "markdown",
@@ -219,7 +322,45 @@
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
"metadata": {},
"outputs": [],
- "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\")"]
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\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.add_edge(START, \"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\")"
+ ]
},
{
"cell_type": "markdown",
@@ -237,7 +378,11 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
+ "source": [
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "\n",
+ "memory = MemorySaver()"
+ ]
},
{
"cell_type": "markdown",
@@ -255,7 +400,12 @@
"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\napp = 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\n",
+ "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -282,7 +432,11 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ndisplay(Image(app.get_graph().draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "display(Image(app.get_graph().draw_mermaid_png()))"
+ ]
},
{
"cell_type": "markdown",
@@ -313,7 +467,14 @@
]
}
],
- "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()"]
+ "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()"
+ ]
},
{
"cell_type": "code",
@@ -334,7 +495,11 @@
]
}
],
- "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()"]
+ "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()"
+ ]
},
{
"cell_type": "code",
@@ -358,7 +523,11 @@
]
}
],
- "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()"]
+ "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()"
+ ]
},
{
"cell_type": "markdown",
@@ -392,7 +561,10 @@
]
}
],
- "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",
@@ -427,7 +599,43 @@
"id": "5454f436-d56e-4499-9381-06192aca1b56",
"metadata": {},
"outputs": [],
- "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"]
+ "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"
+ ]
},
{
"cell_type": "code",
@@ -514,7 +722,43 @@
]
}
],
- "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)"]
+ "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)"
+ ]
},
{
"cell_type": "markdown",
@@ -535,7 +779,34 @@
"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\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 }"]
+ "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",
+ " }"
+ ]
},
{
"cell_type": "markdown",
@@ -551,7 +822,27 @@
"id": "502dc688-c926-407e-8759-8c9e39eb4257",
"metadata": {},
"outputs": [],
- "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)"]
+ "source": [
+ "workflow = StateGraph(State)\n",
+ "\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"action\", call_tool)\n",
+ "\n",
+ "workflow.add_edge(START, \"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)"
+ ]
},
{
"cell_type": "code",
@@ -584,7 +875,13 @@
]
}
],
- "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()"]
+ "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()"
+ ]
},
{
"cell_type": "code",
@@ -617,7 +914,11 @@
]
}
],
- "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()"]
+ "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()"
+ ]
},
{
"cell_type": "code",
@@ -648,7 +949,11 @@
]
}
],
- "source": ["inputs = [HumanMessage(content=\"y\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "inputs = [HumanMessage(content=\"y\")]\n",
+ "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
}
],
"metadata": {
diff --git a/examples/input_output_schema.ipynb b/examples/input_output_schema.ipynb
index 59f902361..16779ba6e 100644
--- a/examples/input_output_schema.ipynb
+++ b/examples/input_output_schema.ipynb
@@ -42,7 +42,6 @@
"def answer_node(state: InputState):\n",
" return {\"answer\": \"bye\"}\n",
"\n",
- "check = SqliteSaver.from_conn_string(\":memory:\")\n",
"graph = StateGraph(input=InputState, output=OutputState)\n",
"graph.add_node(answer_node)\n",
"graph.add_edge(START, \"answer_node\")\n",
diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb
index 58f20760f..d0f269946 100644
--- a/examples/introduction.ipynb
+++ b/examples/introduction.ipynb
@@ -848,7 +848,7 @@
"\n",
"We will see later that **checkpointing** is _much_ more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But before we get too ahead of ourselves, let's add checkpointing to enable multi-turn conversations.\n",
"\n",
- "To get started, create a `SqliteSaver` checkpointer."
+ "To get started, create a `MemorySaver` checkpointer."
]
},
{
@@ -858,9 +858,9 @@
"metadata": {},
"outputs": [],
"source": [
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")"
+ "memory = MemorySaver()"
]
},
{
@@ -868,7 +868,7 @@
"id": "08d3d11a-1b42-4cbb-8e11-2a4294263d90",
"metadata": {},
"source": [
- "**Notice** that we've specified `:memory` as the Sqlite DB path. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to connect to your own DB and/or use one of the other checkpointer classes.\n",
+ "**Notice** we're using an in-memory checkpointer. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect to your own DB.\n",
"\n",
"Next define the graph. Now that you've already built your own `BasicToolNode`, we'll replace it with LangGraph's prebuilt `ToolNode` and `tools_condition`, since these do some nice things like parallel API execution. Apart from that, the following is all copied from Part 2."
]
@@ -1199,7 +1199,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode\n",
@@ -1277,12 +1277,12 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"\n",
"\n",
"class State(TypedDict):\n",
@@ -1508,7 +1508,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode\n",
@@ -1543,7 +1543,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.set_entry_point(\"chatbot\")\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" # This is new!\n",
@@ -1593,7 +1593,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -1627,7 +1627,7 @@
")\n",
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" # This is new!\n",
@@ -2092,7 +2092,7 @@
"from langchain_core.messages import BaseMessage\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -2289,7 +2289,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" # We interrupt before 'human' here instead.\n",
@@ -2539,7 +2539,7 @@
"from langchain_core.pydantic_v1 import BaseModel\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -2626,7 +2626,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
"graph_builder.set_entry_point(\"chatbot\")\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" interrupt_before=[\"human\"],\n",
@@ -2665,11 +2665,11 @@
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
- "from langchain_core.messages import AIMessage, BaseMessage, ToolMessage\n",
+ "from langchain_core.messages import AIMessage, ToolMessage\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"from typing_extensions import TypedDict\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import StateGraph, START\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.prebuilt import ToolNode, tools_condition\n",
@@ -2756,7 +2756,7 @@
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
"graph_builder.add_edge(\"human\", \"chatbot\")\n",
"graph_builder.add_edge(START, \"chatbot\")\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"graph = graph_builder.compile(\n",
" checkpointer=memory,\n",
" interrupt_before=[\"human\"],\n",
diff --git a/examples/memory/add-summary-conversation-history.ipynb b/examples/memory/add-summary-conversation-history.ipynb
index 5b5a19156..10584bc21 100644
--- a/examples/memory/add-summary-conversation-history.ipynb
+++ b/examples/memory/add-summary-conversation-history.ipynb
@@ -105,10 +105,10 @@
"\n",
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.messages import SystemMessage, RemoveMessage\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START, END\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"\n",
"\n",
"# We will add a `summary` attribute (in addition to `messages` key,\n",
diff --git a/examples/memory/delete-messages.ipynb b/examples/memory/delete-messages.ipynb
index feda6f023..dd4cab2eb 100644
--- a/examples/memory/delete-messages.ipynb
+++ b/examples/memory/delete-messages.ipynb
@@ -112,11 +112,11 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"\n",
"\n",
"@tool\n",
diff --git a/examples/memory/manage-conversation-history.ipynb b/examples/memory/manage-conversation-history.ipynb
index 2cdeb85d0..5b5129a15 100644
--- a/examples/memory/manage-conversation-history.ipynb
+++ b/examples/memory/manage-conversation-history.ipynb
@@ -103,11 +103,11 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"\n",
"\n",
"@tool\n",
@@ -234,11 +234,11 @@
"from langchain_anthropic import ChatAnthropic\n",
"from langchain_core.tools import tool\n",
"\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import MessagesState, StateGraph, START\n",
"from langgraph.prebuilt import ToolNode\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"\n",
"\n",
"@tool\n",
diff --git a/examples/persistence.ipynb b/examples/persistence.ipynb
index 03c8911f0..5592ed25d 100644
--- a/examples/persistence.ipynb
+++ b/examples/persistence.ipynb
@@ -17,11 +17,11 @@
"Example:\n",
"```python\n",
"from langgraph.graph import StateGraph\n",
- "from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"builder = StateGraph(....)\n",
"# ... define the graph\n",
- "memory = AsyncSqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)\n",
"...\n",
"```\n",
@@ -361,9 +361,9 @@
"metadata": {},
"outputs": [],
"source": [
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")"
+ "memory = MemorySaver()"
]
},
{
diff --git a/examples/time-travel.ipynb b/examples/time-travel.ipynb
index 4eccaf4e5..dcdd035c0 100644
--- a/examples/time-travel.ipynb
+++ b/examples/time-travel.ipynb
@@ -246,7 +246,7 @@
"id": "6845ed6a-d155-4105-9160-28849877248b",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
+ "source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"]
},
{
"cell_type": "code",
diff --git a/examples/tutorials/rag-agent-testing.ipynb b/examples/tutorials/rag-agent-testing.ipynb
index a1d9ffbef..5d83ae068 100644
--- a/examples/tutorials/rag-agent-testing.ipynb
+++ b/examples/tutorials/rag-agent-testing.ipynb
@@ -574,7 +574,7 @@
}
],
"source": [
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import START, END, StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"from IPython.display import Image, display\n",
@@ -597,7 +597,7 @@
"builder.add_edge(\"tools\", \"assistant\")\n",
"\n",
"# The checkpointer lets the graph persist its state\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"react_graph = builder.compile(checkpointer=memory)\n",
"\n",
"# Show\n",
diff --git a/examples/tutorials/tool-calling-agent-local.ipynb b/examples/tutorials/tool-calling-agent-local.ipynb
index 03659debe..5c60336e8 100644
--- a/examples/tutorials/tool-calling-agent-local.ipynb
+++ b/examples/tutorials/tool-calling-agent-local.ipynb
@@ -253,7 +253,7 @@
"\n",
"\n",
"from IPython.display import Image, display\n",
- "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
"from langgraph.graph import END, START, StateGraph\n",
"from langgraph.prebuilt import tools_condition\n",
"\n",
@@ -275,7 +275,7 @@
"builder.add_edge(\"tools\", \"assistant\")\n",
"\n",
"# The checkpointer lets the graph persist its state\n",
- "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "memory = MemorySaver()\n",
"react_graph = builder.compile(checkpointer=memory)\n",
"\n",
"# Show\n",
diff --git a/examples/usaco/usaco.ipynb b/examples/usaco/usaco.ipynb
index c83c84df6..0a4d4e445 100644
--- a/examples/usaco/usaco.ipynb
+++ b/examples/usaco/usaco.ipynb
@@ -598,7 +598,7 @@
"id": "e6e73e85-1232-4848-beba-3139ac7d0a64",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=checkpointer)"]
+ "source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = MemorySaver()\ngraph = builder.compile(checkpointer=checkpointer)"]
},
{
"cell_type": "code",
@@ -809,7 +809,7 @@
"id": "3c6456ba-363c-4133-8631-6dabb042b6ce",
"metadata": {},
"outputs": [],
- "source": ["# This is all the same as before\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")"]
+ "source": ["# This is all the same as before\nfrom langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = MemorySaver()"]
},
{
"cell_type": "markdown",