diff --git a/examples/branching.ipynb b/examples/branching.ipynb
index cf0024cdd..397e81ef8 100644
--- a/examples/branching.ipynb
+++ b/examples/branching.ipynb
@@ -20,7 +20,10 @@
"id": "bb54e2d0",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langgraph"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph"
+ ]
},
{
"cell_type": "markdown",
@@ -36,7 +39,42 @@
"id": "09372b8b-edea-4b9d-9ec3-3d93ce1ba819",
"metadata": {},
"outputs": [],
- "source": ["import operator\nfrom typing import Annotated, Any\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n\n\nclass ReturnNodeValue:\n def __init__(self, node_secret: str):\n self._value = node_secret\n\n def __call__(self, state: State) -> Any:\n print(f\"Adding {self._value} to {state['aggregate']}\")\n return {\"aggregate\": [self._value]}\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_edge(\"a\", \"b\")\nbuilder.add_edge(\"a\", \"c\")\nbuilder.add_edge(\"b\", \"d\")\nbuilder.add_edge(\"c\", \"d\")\nbuilder.set_finish_point(\"d\")\ngraph = builder.compile()"]
+ "source": [
+ "import operator\n",
+ "from typing import Annotated, Any\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START, END\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " # The operator.add reducer fn makes this append-only\n",
+ " aggregate: Annotated[list, operator.add]\n",
+ "\n",
+ "\n",
+ "class ReturnNodeValue:\n",
+ " def __init__(self, node_secret: str):\n",
+ " self._value = node_secret\n",
+ "\n",
+ " def __call__(self, state: State) -> Any:\n",
+ " print(f\"Adding {self._value} to {state['aggregate']}\")\n",
+ " return {\"aggregate\": [self._value]}\n",
+ "\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
+ "builder.add_edge(START, \"a\")\n",
+ "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
+ "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
+ "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
+ "builder.add_edge(\"a\", \"b\")\n",
+ "builder.add_edge(\"a\", \"c\")\n",
+ "builder.add_edge(\"b\", \"d\")\n",
+ "builder.add_edge(\"c\", \"d\")\n",
+ "builder.add_edge(\"d\", END)\n",
+ "graph = builder.compile()"
+ ]
},
{
"cell_type": "code",
@@ -55,7 +93,11 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -84,7 +126,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})"]
+ "source": [
+ "graph.invoke({\"aggregate\": []}, {\"configurable\": {\"thread_id\": \"foo\"}})"
+ ]
},
{
"cell_type": "markdown",
@@ -118,7 +162,34 @@
"id": "259a7704-5aa0-4e4c-aeef-cca04e8be0ff",
"metadata": {},
"outputs": [],
- "source": ["import operator\nfrom typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_edge(\"a\", \"b\")\nbuilder.add_edge(\"a\", \"c\")\nbuilder.add_edge(\"b\", \"b2\")\nbuilder.add_edge([\"b2\", \"c\"], \"d\")\nbuilder.set_finish_point(\"d\")\ngraph = builder.compile()"]
+ "source": [
+ "import operator\n",
+ "from typing import Annotated\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " # The operator.add reducer fn makes this append-only\n",
+ " aggregate: Annotated[list, operator.add]\n",
+ "\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
+ "builder.add_edge(START, \"a\")\n",
+ "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
+ "builder.add_node(\"b2\", ReturnNodeValue(\"I'm B2\"))\n",
+ "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
+ "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
+ "builder.add_edge(\"a\", \"b\")\n",
+ "builder.add_edge(\"a\", \"c\")\n",
+ "builder.add_edge(\"b\", \"b2\")\n",
+ "builder.add_edge([\"b2\", \"c\"], \"d\")\n",
+ "builder.add_edge(\"d\", END)\n",
+ "graph = builder.compile()"
+ ]
},
{
"cell_type": "code",
@@ -137,7 +208,11 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -167,7 +242,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"aggregate\": []})"]
+ "source": [
+ "graph.invoke({\"aggregate\": []})"
+ ]
},
{
"cell_type": "markdown",
@@ -187,7 +264,49 @@
"id": "95f5e026",
"metadata": {},
"outputs": [],
- "source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, START, StateGraph\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n which: str\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\nbuilder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\nbuilder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\nbuilder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\nbuilder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n\n\ndef route_bc_or_cd(state: State) -> Sequence[str]:\n if state[\"which\"] == \"cd\":\n return [\"c\", \"d\"]\n return [\"b\", \"c\"]\n\n\nintermediates = [\"b\", \"c\", \"d\"]\nbuilder.add_conditional_edges(\n \"a\",\n route_bc_or_cd,\n intermediates,\n)\nfor node in intermediates:\n builder.add_edge(node, \"e\")\n\n\nbuilder.add_edge(\"e\", END)\ngraph = builder.compile()"]
+ "source": [
+ "import operator\n",
+ "from typing import Annotated, Sequence\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import END, START, StateGraph\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " # The operator.add reducer fn makes this append-only\n",
+ " aggregate: Annotated[list, operator.add]\n",
+ " which: str\n",
+ "\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
+ "builder.add_edge(START, \"a\")\n",
+ "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n",
+ "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n",
+ "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n",
+ "builder.add_node(\"e\", ReturnNodeValue(\"I'm E\"))\n",
+ "\n",
+ "\n",
+ "def route_bc_or_cd(state: State) -> Sequence[str]:\n",
+ " if state[\"which\"] == \"cd\":\n",
+ " return [\"c\", \"d\"]\n",
+ " return [\"b\", \"c\"]\n",
+ "\n",
+ "\n",
+ "intermediates = [\"b\", \"c\", \"d\"]\n",
+ "builder.add_conditional_edges(\n",
+ " \"a\",\n",
+ " route_bc_or_cd,\n",
+ " intermediates,\n",
+ ")\n",
+ "for node in intermediates:\n",
+ " builder.add_edge(node, \"e\")\n",
+ "\n",
+ "\n",
+ "builder.add_edge(\"e\", END)\n",
+ "graph = builder.compile()"
+ ]
},
{
"cell_type": "code",
@@ -206,7 +325,11 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -235,7 +358,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"aggregate\": [], \"which\": \"bc\"})"]
+ "source": [
+ "graph.invoke({\"aggregate\": [], \"which\": \"bc\"})"
+ ]
},
{
"cell_type": "code",
@@ -264,7 +389,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"]
+ "source": [
+ "graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"
+ ]
},
{
"cell_type": "markdown",
@@ -286,7 +413,92 @@
"id": "836bc12d",
"metadata": {},
"outputs": [],
- "source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph\n\n\ndef reduce_fanouts(left, right):\n if left is None:\n left = []\n if not right:\n # Overwrite\n return []\n return left + right\n\n\nclass State(TypedDict):\n # The operator.add reducer fn makes this append-only\n aggregate: Annotated[list, operator.add]\n fanout_values: Annotated[list, reduce_fanouts]\n which: str\n\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\nbuilder.add_edge(START, \"a\")\n\n\nclass ParallelReturnNodeValue:\n def __init__(\n self,\n node_secret: str,\n reliability: float,\n ):\n self._value = node_secret\n self._reliability = reliability\n\n def __call__(self, state: State) -> Any:\n print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n return {\n \"fanout_values\": [\n {\n \"value\": [self._value],\n \"reliability\": self._reliability,\n }\n ]\n }\n\n\nbuilder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.9))\n\nbuilder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.1))\nbuilder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.3))\n\n\ndef aggregate_fanout_values(state: State) -> Any:\n # Sort by reliability\n ranked_values = sorted(\n state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n )\n return {\n \"aggregate\": [x[\"value\"] for x in ranked_values] + [\"I'm E\"],\n \"fanout_values\": [],\n }\n\n\nbuilder.add_node(\"e\", aggregate_fanout_values)\n\n\ndef route_bc_or_cd(state: State) -> Sequence[str]:\n if state[\"which\"] == \"cd\":\n return [\"c\", \"d\"]\n return [\"b\", \"c\"]\n\n\nintermediates = [\"b\", \"c\", \"d\"]\nbuilder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n\nfor node in intermediates:\n builder.add_edge(node, \"e\")\n\nbuilder.set_finish_point(\"e\")\ngraph = builder.compile()"]
+ "source": [
+ "import operator\n",
+ "from typing import Annotated, Sequence\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph\n",
+ "\n",
+ "\n",
+ "def reduce_fanouts(left, right):\n",
+ " if left is None:\n",
+ " left = []\n",
+ " if not right:\n",
+ " # Overwrite\n",
+ " return []\n",
+ " return left + right\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " # The operator.add reducer fn makes this append-only\n",
+ " aggregate: Annotated[list, operator.add]\n",
+ " fanout_values: Annotated[list, reduce_fanouts]\n",
+ " which: str\n",
+ "\n",
+ "\n",
+ "builder = StateGraph(State)\n",
+ "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n",
+ "builder.add_edge(START, \"a\")\n",
+ "\n",
+ "\n",
+ "class ParallelReturnNodeValue:\n",
+ " def __init__(\n",
+ " self,\n",
+ " node_secret: str,\n",
+ " reliability: float,\n",
+ " ):\n",
+ " self._value = node_secret\n",
+ " self._reliability = reliability\n",
+ "\n",
+ " def __call__(self, state: State) -> Any:\n",
+ " print(f\"Adding {self._value} to {state['aggregate']} in parallel.\")\n",
+ " return {\n",
+ " \"fanout_values\": [\n",
+ " {\n",
+ " \"value\": [self._value],\n",
+ " \"reliability\": self._reliability,\n",
+ " }\n",
+ " ]\n",
+ " }\n",
+ "\n",
+ "\n",
+ "builder.add_node(\"b\", ParallelReturnNodeValue(\"I'm B\", reliability=0.9))\n",
+ "\n",
+ "builder.add_node(\"c\", ParallelReturnNodeValue(\"I'm C\", reliability=0.1))\n",
+ "builder.add_node(\"d\", ParallelReturnNodeValue(\"I'm D\", reliability=0.3))\n",
+ "\n",
+ "\n",
+ "def aggregate_fanout_values(state: State) -> Any:\n",
+ " # Sort by reliability\n",
+ " ranked_values = sorted(\n",
+ " state[\"fanout_values\"], key=lambda x: x[\"reliability\"], reverse=True\n",
+ " )\n",
+ " return {\n",
+ " \"aggregate\": [x[\"value\"] for x in ranked_values] + [\"I'm E\"],\n",
+ " \"fanout_values\": [],\n",
+ " }\n",
+ "\n",
+ "\n",
+ "builder.add_node(\"e\", aggregate_fanout_values)\n",
+ "\n",
+ "\n",
+ "def route_bc_or_cd(state: State) -> Sequence[str]:\n",
+ " if state[\"which\"] == \"cd\":\n",
+ " return [\"c\", \"d\"]\n",
+ " return [\"b\", \"c\"]\n",
+ "\n",
+ "\n",
+ "intermediates = [\"b\", \"c\", \"d\"]\n",
+ "builder.add_conditional_edges(\"a\", route_bc_or_cd, intermediates)\n",
+ "\n",
+ "for node in intermediates:\n",
+ " builder.add_edge(node, \"e\")\n",
+ "\n",
+ "builder.add_edge(\"e\", END)\n",
+ "graph = builder.compile()"
+ ]
},
{
"cell_type": "code",
@@ -305,7 +517,11 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ndisplay(Image(graph.get_graph().draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "display(Image(graph.get_graph().draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -335,7 +551,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})"]
+ "source": [
+ "graph.invoke({\"aggregate\": [], \"which\": \"bc\", \"fanout_values\": []})"
+ ]
},
{
"cell_type": "code",
@@ -365,7 +583,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"]
+ "source": [
+ "graph.invoke({\"aggregate\": [], \"which\": \"cd\"})"
+ ]
}
],
"metadata": {
diff --git a/examples/cloud_examples/cron_jobs.ipynb b/examples/cloud_examples/cron_jobs.ipynb
index 27f485960..98b6a1b73 100644
--- a/examples/cloud_examples/cron_jobs.ipynb
+++ b/examples/cloud_examples/cron_jobs.ipynb
@@ -20,9 +20,10 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
- "assistants = [a for a in assistants if not a['config']]\n",
+ "assistants = [a for a in assistants if not a[\"config\"]]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
@@ -43,7 +44,12 @@
"outputs": [],
"source": [
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
- "cron_1 = await client.crons.create_for_thread(thread['thread_id'],assistant['assistant_id'],schedule=\"27 15 * * *\",input={'messages':[{\"role\":\"user\",\"content\":\"What time is it?\"}]})"
+ "cron_1 = await client.crons.create_for_thread(\n",
+ " thread[\"thread_id\"],\n",
+ " assistant[\"assistant_id\"],\n",
+ " schedule=\"27 15 * * *\",\n",
+ " input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n",
+ ")"
]
},
{
@@ -59,7 +65,7 @@
"metadata": {},
"outputs": [],
"source": [
- "await client.crons.delete(cron_1['cron_id'])"
+ "await client.crons.delete(cron_1[\"cron_id\"])"
]
},
{
@@ -78,7 +84,11 @@
"outputs": [],
"source": [
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
- "cron_2 = await client.crons.create(assistant['assistant_id'],schedule=\"27 15 * * *\",input={'messages':[{\"role\":\"user\",\"content\":\"What time is it?\"}]})"
+ "cron_2 = await client.crons.create(\n",
+ " assistant[\"assistant_id\"],\n",
+ " schedule=\"27 15 * * *\",\n",
+ " input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n",
+ ")"
]
},
{
@@ -94,7 +104,7 @@
"metadata": {},
"outputs": [],
"source": [
- "await client.crons.delete(cron_2['cron_id'])"
+ "await client.crons.delete(cron_2[\"cron_id\"])"
]
}
],
diff --git a/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb b/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb
index f9f3e5cf4..3193b7fc6 100644
--- a/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb
+++ b/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb
@@ -35,11 +35,12 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
- "assistants = [a for a in assistants if not a['config']]\n",
+ "assistants = [a for a in assistants if not a[\"config\"]]\n",
"assistant = assistants[0]\n",
- "assistant_id = assistant['assistant_id']\n",
+ "assistant_id = assistant[\"assistant_id\"]\n",
"thread = await client.threads.create()"
]
},
diff --git a/examples/cloud_examples/human_in_the_loop_edit_state.ipynb b/examples/cloud_examples/human_in_the_loop_edit_state.ipynb
index 741acb2b8..db4d64424 100644
--- a/examples/cloud_examples/human_in_the_loop_edit_state.ipynb
+++ b/examples/cloud_examples/human_in_the_loop_edit_state.ipynb
@@ -26,9 +26,10 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
- "assistants = [a for a in assistants if not a['config']]\n",
+ "assistants = [a for a in assistants if not a[\"config\"]]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
@@ -58,11 +59,11 @@
}
],
"source": [
- "input = {'messages':[{\"role\":\"user\",\"content\":\"search for weather in SF\"}]}\n",
+ "input = {\"messages\": [{\"role\": \"user\", \"content\": \"search for weather in SF\"}]}\n",
"\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
" interrupt_before=[\"action\"],\n",
@@ -99,14 +100,14 @@
],
"source": [
"# First, lets get the current state\n",
- "current_state = await client.threads.get_state(thread['thread_id'])\n",
+ "current_state = await client.threads.get_state(thread[\"thread_id\"])\n",
"\n",
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
- "last_message = current_state['values']['messages'][-1]\n",
+ "last_message = current_state[\"values\"][\"messages\"][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
- "last_message['tool_calls'][0]['args'] = {'query': 'current weather in Sidi Frej'}\n",
+ "last_message[\"tool_calls\"][0][\"args\"] = {\"query\": \"current weather in Sidi Frej\"}\n",
"\n",
"# Let's now call `update_state` to pass in this message in the `messages` key\n",
"# This will get treated as any other update to the state\n",
@@ -114,7 +115,7 @@
"# That reducer function will use the ID of the message to update it\n",
"# It's important that it has the right ID! Otherwise it would get appended\n",
"# as a new message\n",
- "await client.threads.update_state(thread['thread_id'], {\"messages\": last_message})"
+ "await client.threads.update_state(thread[\"thread_id\"], {\"messages\": last_message})"
]
},
{
@@ -145,7 +146,7 @@
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
"):\n",
diff --git a/examples/cloud_examples/human_in_the_loop_time_travel.ipynb b/examples/cloud_examples/human_in_the_loop_time_travel.ipynb
index 77450204c..80d19c595 100644
--- a/examples/cloud_examples/human_in_the_loop_time_travel.ipynb
+++ b/examples/cloud_examples/human_in_the_loop_time_travel.ipynb
@@ -24,9 +24,10 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
- "assistants = [a for a in assistants if not a['config']]\n",
+ "assistants = [a for a in assistants if not a[\"config\"]]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
@@ -58,11 +59,11 @@
}
],
"source": [
- "input = {'messages':[{\"role\":\"user\",\"content\":\"Please search the weather in SF\"}]}\n",
+ "input = {\"messages\": [{\"role\": \"user\", \"content\": \"Please search the weather in SF\"}]}\n",
"\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
@@ -83,7 +84,7 @@
"metadata": {},
"outputs": [],
"source": [
- "states = await client.threads.get_history(thread['thread_id'])"
+ "states = await client.threads.get_history(thread[\"thread_id\"])"
]
},
{
@@ -105,7 +106,7 @@
"source": [
"# We can confirm that this state is correct by checking the 'next' attribute and seeing that it is the tool call node\n",
"state_to_replay = states[2]\n",
- "state_to_replay['next']"
+ "state_to_replay[\"next\"]"
]
},
{
@@ -132,10 +133,10 @@
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
- " config={\"configurable\":{\"thread_ts\":state_to_replay['checkpoint_id']}}\n",
+ " config={\"configurable\": {\"thread_ts\": state_to_replay[\"checkpoint_id\"]}},\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
@@ -162,12 +163,16 @@
"source": [
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
- "last_message = state_to_replay['values']['messages'][-1]\n",
+ "last_message = state_to_replay[\"values\"][\"messages\"][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
- "last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'}\n",
+ "last_message[\"tool_calls\"][0][\"args\"] = {\"query\": \"current weather in SF\"}\n",
"\n",
- "new_state = await client.threads.update_state(thread['thread_id'],{\"messages\":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])"
+ "new_state = await client.threads.update_state(\n",
+ " thread[\"thread_id\"],\n",
+ " {\"messages\": [last_message]},\n",
+ " checkpoint_id=state_to_replay[\"checkpoint_id\"],\n",
+ ")"
]
},
{
@@ -194,10 +199,10 @@
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
- " config={\"configurable\":{\"thread_ts\":new_state['configurable']['thread_ts']}}\n",
+ " config={\"configurable\": {\"thread_ts\": new_state[\"configurable\"][\"thread_ts\"]}},\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
diff --git a/examples/cloud_examples/human_in_the_loop_user_input.ipynb b/examples/cloud_examples/human_in_the_loop_user_input.ipynb
index 0d70e8db3..e5e49ecd5 100644
--- a/examples/cloud_examples/human_in_the_loop_user_input.ipynb
+++ b/examples/cloud_examples/human_in_the_loop_user_input.ipynb
@@ -44,9 +44,10 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
- "assistants = [a for a in assistants if not a['config']]\n",
+ "assistants = [a for a in assistants if not a[\"config\"]]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
@@ -76,11 +77,18 @@
}
],
"source": [
- "input = {'messages':[{\"role\":\"user\",\"content\":\"Use the search tool to ask the user where they are, then look up the weather there\"}]}\n",
+ "input = {\n",
+ " \"messages\": [\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"Use the search tool to ask the user where they are, then look up the weather there\",\n",
+ " }\n",
+ " ]\n",
+ "}\n",
"\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
" interrupt_before=[\"ask_human\"],\n",
@@ -118,13 +126,17 @@
}
],
"source": [
- "state = await client.threads.get_state(thread['thread_id'])\n",
- "tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']\n",
+ "state = await client.threads.get_state(thread[\"thread_id\"])\n",
+ "tool_call_id = state[\"values\"][\"messages\"][-1][\"tool_calls\"][0][\"id\"]\n",
"\n",
"# We now create the tool call with the id and the response we want\n",
- "tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n",
+ "tool_message = [\n",
+ " {\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}\n",
+ "]\n",
"\n",
- "await client.threads.update_state(thread['thread_id'], {\"messages\": tool_message}, as_node=\"ask_human\")\n"
+ "await client.threads.update_state(\n",
+ " thread[\"thread_id\"], {\"messages\": tool_message}, as_node=\"ask_human\"\n",
+ ")"
]
},
{
@@ -154,7 +166,7 @@
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
"):\n",
diff --git a/examples/cloud_examples/langgraph_to_langgraph_cloud.ipynb b/examples/cloud_examples/langgraph_to_langgraph_cloud.ipynb
index e85e2999d..20011991b 100644
--- a/examples/cloud_examples/langgraph_to_langgraph_cloud.ipynb
+++ b/examples/cloud_examples/langgraph_to_langgraph_cloud.ipynb
@@ -119,6 +119,7 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()"
]
},
@@ -354,7 +355,9 @@
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
- "async for chunk in client.runs.stream(None, \"agent\", input=inputs, stream_mode=\"values\"):\n",
+ "async for chunk in client.runs.stream(\n",
+ " None, \"agent\", input=inputs, stream_mode=\"values\"\n",
+ "):\n",
" if chunk.event == \"values\":\n",
" messages = convert_to_messages(chunk.data[\"messages\"])\n",
" messages[-1].pretty_print()"
@@ -423,7 +426,9 @@
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in nyc\")]}\n",
- "invoke_output = await graph_with_memory.ainvoke(inputs, config={\"configurable\": {\"thread_id\": \"1\"}})\n",
+ "invoke_output = await graph_with_memory.ainvoke(\n",
+ " inputs, config={\"configurable\": {\"thread_id\": \"1\"}}\n",
+ ")\n",
"invoke_output[\"messages\"][-1].pretty_print()"
]
},
@@ -458,7 +463,9 @@
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's it known for?\")]}\n",
- "invoke_output = await graph_with_memory.ainvoke(inputs, config={\"configurable\": {\"thread_id\": \"1\"}})\n",
+ "invoke_output = await graph_with_memory.ainvoke(\n",
+ " inputs, config={\"configurable\": {\"thread_id\": \"1\"}}\n",
+ ")\n",
"invoke_output[\"messages\"][-1].pretty_print()"
]
},
@@ -480,7 +487,9 @@
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's it known for?\")]}\n",
- "invoke_output = await graph_with_memory.ainvoke(inputs, config={\"configurable\": {\"thread_id\": \"2\"}})\n",
+ "invoke_output = await graph_with_memory.ainvoke(\n",
+ " inputs, config={\"configurable\": {\"thread_id\": \"2\"}}\n",
+ ")\n",
"invoke_output[\"messages\"][-1].pretty_print()"
]
},
@@ -724,7 +733,12 @@
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
- "async for chunk in graph_with_memory.astream(inputs, stream_mode=\"values\", interrupt_before=[\"tools\"], config={\"configurable\": {\"thread_id\": \"3\"}}):\n",
+ "async for chunk in graph_with_memory.astream(\n",
+ " inputs,\n",
+ " stream_mode=\"values\",\n",
+ " interrupt_before=[\"tools\"],\n",
+ " config={\"configurable\": {\"thread_id\": \"3\"}},\n",
+ "):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
},
@@ -749,7 +763,12 @@
}
],
"source": [
- "async for chunk in graph_with_memory.astream(None, stream_mode=\"values\", interrupt_before=[\"tools\"], config={\"configurable\": {\"thread_id\": \"3\"}}):\n",
+ "async for chunk in graph_with_memory.astream(\n",
+ " None,\n",
+ " stream_mode=\"values\",\n",
+ " interrupt_before=[\"tools\"],\n",
+ " config={\"configurable\": {\"thread_id\": \"3\"}},\n",
+ "):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
},
@@ -794,7 +813,13 @@
"source": [
"thread = await client.threads.create()\n",
"\n",
- "async for chunk in client.runs.stream(thread[\"thread_id\"], \"agent\", input=inputs, stream_mode=\"values\", interrupt_before=[\"tools\"]):\n",
+ "async for chunk in client.runs.stream(\n",
+ " thread[\"thread_id\"],\n",
+ " \"agent\",\n",
+ " input=inputs,\n",
+ " stream_mode=\"values\",\n",
+ " interrupt_before=[\"tools\"],\n",
+ "):\n",
" if chunk.event == \"values\":\n",
" messages = convert_to_messages(chunk.data[\"messages\"])\n",
" messages[-1].pretty_print()"
@@ -821,7 +846,13 @@
}
],
"source": [
- "async for chunk in client.runs.stream(thread[\"thread_id\"], \"agent\", input=None, stream_mode=\"values\", interrupt_before=[\"tools\"]):\n",
+ "async for chunk in client.runs.stream(\n",
+ " thread[\"thread_id\"],\n",
+ " \"agent\",\n",
+ " input=None,\n",
+ " stream_mode=\"values\",\n",
+ " interrupt_before=[\"tools\"],\n",
+ "):\n",
" if chunk.event == \"values\":\n",
" messages = convert_to_messages(chunk.data[\"messages\"])\n",
" messages[-1].pretty_print()"
@@ -1000,7 +1031,9 @@
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
- "async for chunk in client.runs.stream(None, \"agent\", input=inputs, stream_mode=\"events\"):\n",
+ "async for chunk in client.runs.stream(\n",
+ " None, \"agent\", input=inputs, stream_mode=\"events\"\n",
+ "):\n",
" if chunk.event == \"events\" and chunk.data[\"event\"] == \"on_chat_model_stream\":\n",
" print(chunk.data[\"data\"][\"chunk\"])"
]
diff --git a/examples/cloud_examples/stateless_runs.ipynb b/examples/cloud_examples/stateless_runs.ipynb
index fe2117850..5aa704e35 100644
--- a/examples/cloud_examples/stateless_runs.ipynb
+++ b/examples/cloud_examples/stateless_runs.ipynb
@@ -20,9 +20,10 @@
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
+ "\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
- "assistants = [a for a in assistants if not a['config']]\n",
+ "assistants = [a for a in assistants if not a[\"config\"]]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
@@ -50,13 +51,17 @@
}
],
"source": [
- "input = {\"messages\":[{\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}\n",
+ "input = {\n",
+ " \"messages\": [\n",
+ " {\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}\n",
+ " ]\n",
+ "}\n",
"\n",
"\n",
"async for chunk in client.runs.stream(\n",
" # Don't pass in a thread_id and the stream will be stateless\n",
" None,\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
@@ -81,7 +86,7 @@
"source": [
"stateless_run_result = await client.runs.wait(\n",
" None,\n",
- " assistant[\"assistant_id\"], # graph_id\n",
+ " assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
")"
]
diff --git a/examples/create-react-agent-hitl.ipynb b/examples/create-react-agent-hitl.ipynb
index fd0ef87de..0d9e46cc1 100644
--- a/examples/create-react-agent-hitl.ipynb
+++ b/examples/create-react-agent-hitl.ipynb
@@ -113,7 +113,9 @@
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
- "graph = create_react_agent(model, tools=tools, interrupt_before=[\"tools\"], checkpointer=memory)"
+ "graph = create_react_agent(\n",
+ " model, tools=tools, interrupt_before=[\"tools\"], checkpointer=memory\n",
+ ")"
]
},
{
diff --git a/examples/extraction/retries.ipynb b/examples/extraction/retries.ipynb
index 2d240c9ad..3e0dcce31 100644
--- a/examples/extraction/retries.ipynb
+++ b/examples/extraction/retries.ipynb
@@ -32,7 +32,11 @@
"id": "0ada5e8f-3f2f-459e-83aa-6cd8861770dd",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langchain-anthropic langgraph\n# Or do langchain-{groq|openai|etc.} for another package with tool calling"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langchain-anthropic langgraph\n",
+ "# Or do langchain-{groq|openai|etc.} for another package with tool calling"
+ ]
},
{
"cell_type": "markdown",
@@ -48,7 +52,22 @@
"id": "c0acb818-b6fd-48ab-97e6-fc2de2d03e87",
"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\")\n# Recommended to visualize the retry steps\n_set_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Extraction Notebook\""]
+ "source": [
+ "import getpass\n",
+ "import os\n",
+ "\n",
+ "\n",
+ "def _set_env(var: str):\n",
+ " if not os.environ.get(var):\n",
+ " os.environ[var] = getpass.getpass(f\"{var}: \")\n",
+ "\n",
+ "\n",
+ "_set_env(\"OPENAI_API_KEY\")\n",
+ "# Recommended to visualize the retry steps\n",
+ "_set_env(\"LANGCHAIN_API_KEY\")\n",
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
+ "os.environ[\"LANGCHAIN_PROJECT\"] = \"Extraction Notebook\""
+ ]
},
{
"cell_type": "markdown",
@@ -64,7 +83,289 @@
"id": "baf669a0-04ee-492d-80d8-8fcb658ed128",
"metadata": {},
"outputs": [],
- "source": ["import operator\nimport uuid\nfrom typing import (\n Annotated,\n Any,\n Callable,\n Dict,\n List,\n Literal,\n Optional,\n Sequence,\n Type,\n Union,\n)\n\nfrom langchain_core.language_models import BaseChatModel\nfrom langchain_core.messages import (\n AIMessage,\n AnyMessage,\n BaseMessage,\n HumanMessage,\n ToolCall,\n)\nfrom langchain_core.prompt_values import PromptValue\nfrom langchain_core.runnables import (\n Runnable,\n RunnableLambda,\n)\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ValidationNode\n\n\ndef _default_aggregator(messages: Sequence[AnyMessage]) -> AIMessage:\n for m in messages[::-1]:\n if m.type == \"ai\":\n return m\n raise ValueError(\"No AI message found in the sequence.\")\n\n\nclass RetryStrategy(TypedDict, total=False):\n \"\"\"The retry strategy for a tool call.\"\"\"\n\n max_attempts: int\n \"\"\"The maximum number of attempts to make.\"\"\"\n fallback: Optional[\n Union[\n Runnable[Sequence[AnyMessage], AIMessage],\n Runnable[Sequence[AnyMessage], BaseMessage],\n Callable[[Sequence[AnyMessage]], AIMessage],\n ]\n ]\n \"\"\"The function to use once validation fails.\"\"\"\n aggregate_messages: Optional[Callable[[Sequence[AnyMessage]], AIMessage]]\n\n\ndef _bind_validator_with_retries(\n llm: Union[\n Runnable[Sequence[AnyMessage], AIMessage],\n Runnable[Sequence[BaseMessage], BaseMessage],\n ],\n *,\n validator: ValidationNode,\n retry_strategy: RetryStrategy,\n tool_choice: Optional[str] = None,\n) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n \"\"\"Binds a tool validators + retry logic to create a runnable validation graph.\n\n LLMs that support tool calling can generate structured JSON. However, they may not always\n perfectly follow your requested schema, especially if the schema is nested or has complex\n validation rules. This method allows you to bind a validation function to the LLM's output,\n so that any time the LLM generates a message, the validation function is run on it. If\n the validation fails, the method will retry the LLM with a fallback strategy, the simplest\n being just to add a message to the output with the validation errors and a request to fix them.\n\n The resulting runnable expects a list of messages as input and returns a single AI message.\n By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n your existing chat bot. You can specify a tool_choice to force the validator to be run on\n the outputs.\n\n Args:\n llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n validator (ValidationNode): The validation logic.\n retry_strategy (RetryStrategy): The retry strategy to use.\n Possible keys:\n - max_attempts: The maximum number of attempts to make.\n - fallback: The LLM or function to use in case of validation failure.\n - aggregate_messages: A function to aggregate the messages over multiple turns.\n Defaults to fetching the last AI message.\n tool_choice: If provided, always run the validator on the tool output.\n\n Returns:\n Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n \"\"\"\n\n def add_or_overwrite_messages(left: list, right: Union[list, dict]) -> list:\n \"\"\"Append messages. If the update is a 'finalized' output, replace the whole list.\"\"\"\n if isinstance(right, dict) and \"finalize\" in right:\n finalized = right[\"finalize\"]\n if not isinstance(finalized, list):\n finalized = [finalized]\n for m in finalized:\n if m.id is None:\n m.id = str(uuid.uuid4())\n return finalized\n res = add_messages(left, right)\n if not isinstance(res, list):\n return [res]\n return res\n\n class State(TypedDict):\n messages: Annotated[list, add_or_overwrite_messages]\n attempt_number: Annotated[int, operator.add]\n initial_num_messages: int\n input_format: Literal[\"list\", \"dict\"]\n\n builder = StateGraph(State)\n\n def dedict(x: State) -> list:\n \"\"\"Get the messages from the state.\"\"\"\n return x[\"messages\"]\n\n model = dedict | llm | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n fbrunnable = retry_strategy.get(\"fallback\")\n if fbrunnable is None:\n fb_runnable = llm\n elif isinstance(fbrunnable, Runnable):\n fb_runnable = fbrunnable # type: ignore\n else:\n fb_runnable = RunnableLambda(fbrunnable)\n fallback = (\n dedict | fb_runnable | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n )\n\n def count_messages(state: State) -> dict:\n return {\"initial_num_messages\": len(state.get(\"messages\", []))}\n\n builder.add_node(\"count_messages\", count_messages)\n builder.add_node(\"llm\", model)\n builder.add_node(\"fallback\", fallback)\n\n # To support patch-based retries, we need to be able to\n # aggregate the messages over multiple turns.\n # The next sequence selects only the relevant messages\n # and then applies the validator\n select_messages = retry_strategy.get(\"aggregate_messages\") or _default_aggregator\n\n def select_generated_messages(state: State) -> list:\n \"\"\"Select only the messages generated within this loop.\"\"\"\n selected = state[\"messages\"][state[\"initial_num_messages\"] :]\n return [select_messages(selected)]\n\n def endict_validator_output(x: Sequence[AnyMessage]) -> dict:\n if tool_choice and not x:\n return {\n \"messages\": [\n HumanMessage(\n content=f\"ValidationError: please respond with a valid tool call [tool_choice={tool_choice}].\",\n additional_kwargs={\"is_error\": True},\n )\n ]\n }\n return {\"messages\": x}\n\n validator_runnable = select_generated_messages | validator | endict_validator_output\n builder.add_node(\"validator\", validator_runnable)\n\n class Finalizer:\n \"\"\"Pick the final message to return from the retry loop.\"\"\"\n\n def __init__(self, aggregator: Optional[Callable[[list], AIMessage]] = None):\n self._aggregator = aggregator or _default_aggregator\n\n def __call__(self, state: State) -> dict:\n \"\"\"Return just the AI message.\"\"\"\n initial_num_messages = state[\"initial_num_messages\"]\n generated_messages = state[\"messages\"][initial_num_messages:]\n return {\n \"messages\": {\n \"finalize\": self._aggregator(generated_messages),\n }\n }\n\n # We only want to emit the final message\n builder.add_node(\"finalizer\", Finalizer(retry_strategy.get(\"aggregate_messages\")))\n\n # Define the connectivity\n builder.add_edge(START, \"count_messages\")\n builder.add_edge(\"count_messages\", \"llm\")\n\n def route_validator(state: State) -> Literal[\"validator\", \"__end__\"]:\n if state[\"messages\"][-1].tool_calls or tool_choice is not None:\n return \"validator\"\n return \"__end__\"\n\n builder.add_conditional_edges(\"llm\", route_validator)\n builder.add_edge(\"fallback\", \"validator\")\n max_attempts = retry_strategy.get(\"max_attempts\", 3)\n\n def route_validation(state: State) -> Literal[\"finalizer\", \"fallback\"]:\n if state[\"attempt_number\"] > max_attempts:\n raise ValueError(\n f\"Could not extract a valid value in {max_attempts} attempts.\"\n )\n for m in state[\"messages\"][::-1]:\n if m.type == \"ai\":\n break\n if m.additional_kwargs.get(\"is_error\"):\n return \"fallback\"\n return \"finalizer\"\n\n builder.add_conditional_edges(\"validator\", route_validation)\n\n builder.set_finish_point(\"finalizer\")\n\n # These functions let the step be used in a MessageGraph\n # or a StateGraph with 'messages' as the key.\n def encode(x: Union[Sequence[AnyMessage], PromptValue]) -> dict:\n \"\"\"Ensure the input is the correct format.\"\"\"\n if isinstance(x, PromptValue):\n return {\"messages\": x.to_messages(), \"input_format\": \"list\"}\n if isinstance(x, list):\n return {\"messages\": x, \"input_format\": \"list\"}\n raise ValueError(f\"Unexpected input type: {type(x)}\")\n\n def decode(x: State) -> AIMessage:\n \"\"\"Ensure the output is in the expected format.\"\"\"\n return x[\"messages\"][-1]\n\n return (\n encode | builder.compile().with_config(run_name=\"ValidationGraph\") | decode\n ).with_config(run_name=\"ValidateWithRetries\")\n\n\ndef bind_validator_with_retries(\n llm: BaseChatModel,\n *,\n tools: list,\n tool_choice: Optional[str] = None,\n max_attempts: int = 3,\n) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n\n LLMs that support tool calling are good at generating structured JSON. However, they may\n not always perfectly follow your requested schema, especially if the schema is nested or\n has complex validation rules. This method allows you to bind a validation function to\n the LLM's output, so that any time the LLM generates a message, the validation function\n is run on it. If the validation fails, the method will retry the LLM with a fallback\n strategy, the simples being just to add a message to the output with the validation\n errors and a request to fix them.\n\n The resulting runnable expects a list of messages as input and returns a single AI message.\n By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n your existing chat bot. You can specify a tool_choice to force the validator to be run on\n the outputs.\n\n Args:\n llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n validator (ValidationNode): The validation logic.\n retry_strategy (RetryStrategy): The retry strategy to use.\n Possible keys:\n - max_attempts: The maximum number of attempts to make.\n - fallback: The LLM or function to use in case of validation failure.\n - aggregate_messages: A function to aggregate the messages over multiple turns.\n Defaults to fetching the last AI message.\n tool_choice: If provided, always run the validator on the tool output.\n\n Returns:\n Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n \"\"\"\n bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n retry_strategy = RetryStrategy(max_attempts=max_attempts)\n validator = ValidationNode(tools)\n return _bind_validator_with_retries(\n bound_llm,\n validator=validator,\n tool_choice=tool_choice,\n retry_strategy=retry_strategy,\n ).with_config(metadata={\"retry_strategy\": \"default\"})"]
+ "source": [
+ "import operator\n",
+ "import uuid\n",
+ "from typing import (\n",
+ " Annotated,\n",
+ " Any,\n",
+ " Callable,\n",
+ " Dict,\n",
+ " List,\n",
+ " Literal,\n",
+ " Optional,\n",
+ " Sequence,\n",
+ " Type,\n",
+ " Union,\n",
+ ")\n",
+ "\n",
+ "from langchain_core.language_models import BaseChatModel\n",
+ "from langchain_core.messages import (\n",
+ " AIMessage,\n",
+ " AnyMessage,\n",
+ " BaseMessage,\n",
+ " HumanMessage,\n",
+ " ToolCall,\n",
+ ")\n",
+ "from langchain_core.prompt_values import PromptValue\n",
+ "from langchain_core.runnables import (\n",
+ " Runnable,\n",
+ " RunnableLambda,\n",
+ ")\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START, END\n",
+ "from langgraph.graph.message import add_messages\n",
+ "from langgraph.prebuilt import ValidationNode\n",
+ "\n",
+ "\n",
+ "def _default_aggregator(messages: Sequence[AnyMessage]) -> AIMessage:\n",
+ " for m in messages[::-1]:\n",
+ " if m.type == \"ai\":\n",
+ " return m\n",
+ " raise ValueError(\"No AI message found in the sequence.\")\n",
+ "\n",
+ "\n",
+ "class RetryStrategy(TypedDict, total=False):\n",
+ " \"\"\"The retry strategy for a tool call.\"\"\"\n",
+ "\n",
+ " max_attempts: int\n",
+ " \"\"\"The maximum number of attempts to make.\"\"\"\n",
+ " fallback: Optional[\n",
+ " Union[\n",
+ " Runnable[Sequence[AnyMessage], AIMessage],\n",
+ " Runnable[Sequence[AnyMessage], BaseMessage],\n",
+ " Callable[[Sequence[AnyMessage]], AIMessage],\n",
+ " ]\n",
+ " ]\n",
+ " \"\"\"The function to use once validation fails.\"\"\"\n",
+ " aggregate_messages: Optional[Callable[[Sequence[AnyMessage]], AIMessage]]\n",
+ "\n",
+ "\n",
+ "def _bind_validator_with_retries(\n",
+ " llm: Union[\n",
+ " Runnable[Sequence[AnyMessage], AIMessage],\n",
+ " Runnable[Sequence[BaseMessage], BaseMessage],\n",
+ " ],\n",
+ " *,\n",
+ " validator: ValidationNode,\n",
+ " retry_strategy: RetryStrategy,\n",
+ " tool_choice: Optional[str] = None,\n",
+ ") -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n",
+ " \"\"\"Binds a tool validators + retry logic to create a runnable validation graph.\n",
+ "\n",
+ " LLMs that support tool calling can generate structured JSON. However, they may not always\n",
+ " perfectly follow your requested schema, especially if the schema is nested or has complex\n",
+ " validation rules. This method allows you to bind a validation function to the LLM's output,\n",
+ " so that any time the LLM generates a message, the validation function is run on it. If\n",
+ " the validation fails, the method will retry the LLM with a fallback strategy, the simplest\n",
+ " being just to add a message to the output with the validation errors and a request to fix them.\n",
+ "\n",
+ " The resulting runnable expects a list of messages as input and returns a single AI message.\n",
+ " By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n",
+ " your existing chat bot. You can specify a tool_choice to force the validator to be run on\n",
+ " the outputs.\n",
+ "\n",
+ " Args:\n",
+ " llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n",
+ " validator (ValidationNode): The validation logic.\n",
+ " retry_strategy (RetryStrategy): The retry strategy to use.\n",
+ " Possible keys:\n",
+ " - max_attempts: The maximum number of attempts to make.\n",
+ " - fallback: The LLM or function to use in case of validation failure.\n",
+ " - aggregate_messages: A function to aggregate the messages over multiple turns.\n",
+ " Defaults to fetching the last AI message.\n",
+ " tool_choice: If provided, always run the validator on the tool output.\n",
+ "\n",
+ " Returns:\n",
+ " Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n",
+ " \"\"\"\n",
+ "\n",
+ " def add_or_overwrite_messages(left: list, right: Union[list, dict]) -> list:\n",
+ " \"\"\"Append messages. If the update is a 'finalized' output, replace the whole list.\"\"\"\n",
+ " if isinstance(right, dict) and \"finalize\" in right:\n",
+ " finalized = right[\"finalize\"]\n",
+ " if not isinstance(finalized, list):\n",
+ " finalized = [finalized]\n",
+ " for m in finalized:\n",
+ " if m.id is None:\n",
+ " m.id = str(uuid.uuid4())\n",
+ " return finalized\n",
+ " res = add_messages(left, right)\n",
+ " if not isinstance(res, list):\n",
+ " return [res]\n",
+ " return res\n",
+ "\n",
+ " class State(TypedDict):\n",
+ " messages: Annotated[list, add_or_overwrite_messages]\n",
+ " attempt_number: Annotated[int, operator.add]\n",
+ " initial_num_messages: int\n",
+ " input_format: Literal[\"list\", \"dict\"]\n",
+ "\n",
+ " builder = StateGraph(State)\n",
+ "\n",
+ " def dedict(x: State) -> list:\n",
+ " \"\"\"Get the messages from the state.\"\"\"\n",
+ " return x[\"messages\"]\n",
+ "\n",
+ " model = dedict | llm | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n",
+ " fbrunnable = retry_strategy.get(\"fallback\")\n",
+ " if fbrunnable is None:\n",
+ " fb_runnable = llm\n",
+ " elif isinstance(fbrunnable, Runnable):\n",
+ " fb_runnable = fbrunnable # type: ignore\n",
+ " else:\n",
+ " fb_runnable = RunnableLambda(fbrunnable)\n",
+ " fallback = (\n",
+ " dedict | fb_runnable | (lambda msg: {\"messages\": [msg], \"attempt_number\": 1})\n",
+ " )\n",
+ "\n",
+ " def count_messages(state: State) -> dict:\n",
+ " return {\"initial_num_messages\": len(state.get(\"messages\", []))}\n",
+ "\n",
+ " builder.add_node(\"count_messages\", count_messages)\n",
+ " builder.add_node(\"llm\", model)\n",
+ " builder.add_node(\"fallback\", fallback)\n",
+ "\n",
+ " # To support patch-based retries, we need to be able to\n",
+ " # aggregate the messages over multiple turns.\n",
+ " # The next sequence selects only the relevant messages\n",
+ " # and then applies the validator\n",
+ " select_messages = retry_strategy.get(\"aggregate_messages\") or _default_aggregator\n",
+ "\n",
+ " def select_generated_messages(state: State) -> list:\n",
+ " \"\"\"Select only the messages generated within this loop.\"\"\"\n",
+ " selected = state[\"messages\"][state[\"initial_num_messages\"] :]\n",
+ " return [select_messages(selected)]\n",
+ "\n",
+ " def endict_validator_output(x: Sequence[AnyMessage]) -> dict:\n",
+ " if tool_choice and not x:\n",
+ " return {\n",
+ " \"messages\": [\n",
+ " HumanMessage(\n",
+ " content=f\"ValidationError: please respond with a valid tool call [tool_choice={tool_choice}].\",\n",
+ " additional_kwargs={\"is_error\": True},\n",
+ " )\n",
+ " ]\n",
+ " }\n",
+ " return {\"messages\": x}\n",
+ "\n",
+ " validator_runnable = select_generated_messages | validator | endict_validator_output\n",
+ " builder.add_node(\"validator\", validator_runnable)\n",
+ "\n",
+ " class Finalizer:\n",
+ " \"\"\"Pick the final message to return from the retry loop.\"\"\"\n",
+ "\n",
+ " def __init__(self, aggregator: Optional[Callable[[list], AIMessage]] = None):\n",
+ " self._aggregator = aggregator or _default_aggregator\n",
+ "\n",
+ " def __call__(self, state: State) -> dict:\n",
+ " \"\"\"Return just the AI message.\"\"\"\n",
+ " initial_num_messages = state[\"initial_num_messages\"]\n",
+ " generated_messages = state[\"messages\"][initial_num_messages:]\n",
+ " return {\n",
+ " \"messages\": {\n",
+ " \"finalize\": self._aggregator(generated_messages),\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " # We only want to emit the final message\n",
+ " builder.add_node(\"finalizer\", Finalizer(retry_strategy.get(\"aggregate_messages\")))\n",
+ "\n",
+ " # Define the connectivity\n",
+ " builder.add_edge(START, \"count_messages\")\n",
+ " builder.add_edge(\"count_messages\", \"llm\")\n",
+ "\n",
+ " def route_validator(state: State) -> Literal[\"validator\", \"__end__\"]:\n",
+ " if state[\"messages\"][-1].tool_calls or tool_choice is not None:\n",
+ " return \"validator\"\n",
+ " return \"__end__\"\n",
+ "\n",
+ " builder.add_conditional_edges(\"llm\", route_validator)\n",
+ " builder.add_edge(\"fallback\", \"validator\")\n",
+ " max_attempts = retry_strategy.get(\"max_attempts\", 3)\n",
+ "\n",
+ " def route_validation(state: State) -> Literal[\"finalizer\", \"fallback\"]:\n",
+ " if state[\"attempt_number\"] > max_attempts:\n",
+ " raise ValueError(\n",
+ " f\"Could not extract a valid value in {max_attempts} attempts.\"\n",
+ " )\n",
+ " for m in state[\"messages\"][::-1]:\n",
+ " if m.type == \"ai\":\n",
+ " break\n",
+ " if m.additional_kwargs.get(\"is_error\"):\n",
+ " return \"fallback\"\n",
+ " return \"finalizer\"\n",
+ "\n",
+ " builder.add_conditional_edges(\"validator\", route_validation)\n",
+ "\n",
+ " builder.add_edge(\"finalizer\", END)\n",
+ "\n",
+ " # These functions let the step be used in a MessageGraph\n",
+ " # or a StateGraph with 'messages' as the key.\n",
+ " def encode(x: Union[Sequence[AnyMessage], PromptValue]) -> dict:\n",
+ " \"\"\"Ensure the input is the correct format.\"\"\"\n",
+ " if isinstance(x, PromptValue):\n",
+ " return {\"messages\": x.to_messages(), \"input_format\": \"list\"}\n",
+ " if isinstance(x, list):\n",
+ " return {\"messages\": x, \"input_format\": \"list\"}\n",
+ " raise ValueError(f\"Unexpected input type: {type(x)}\")\n",
+ "\n",
+ " def decode(x: State) -> AIMessage:\n",
+ " \"\"\"Ensure the output is in the expected format.\"\"\"\n",
+ " return x[\"messages\"][-1]\n",
+ "\n",
+ " return (\n",
+ " encode | builder.compile().with_config(run_name=\"ValidationGraph\") | decode\n",
+ " ).with_config(run_name=\"ValidateWithRetries\")\n",
+ "\n",
+ "\n",
+ "def bind_validator_with_retries(\n",
+ " llm: BaseChatModel,\n",
+ " *,\n",
+ " tools: list,\n",
+ " tool_choice: Optional[str] = None,\n",
+ " max_attempts: int = 3,\n",
+ ") -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n",
+ " \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n",
+ "\n",
+ " LLMs that support tool calling are good at generating structured JSON. However, they may\n",
+ " not always perfectly follow your requested schema, especially if the schema is nested or\n",
+ " has complex validation rules. This method allows you to bind a validation function to\n",
+ " the LLM's output, so that any time the LLM generates a message, the validation function\n",
+ " is run on it. If the validation fails, the method will retry the LLM with a fallback\n",
+ " strategy, the simples being just to add a message to the output with the validation\n",
+ " errors and a request to fix them.\n",
+ "\n",
+ " The resulting runnable expects a list of messages as input and returns a single AI message.\n",
+ " By default, the LLM can optionally NOT invoke tools, making this easier to incorporate into\n",
+ " your existing chat bot. You can specify a tool_choice to force the validator to be run on\n",
+ " the outputs.\n",
+ "\n",
+ " Args:\n",
+ " llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n",
+ " validator (ValidationNode): The validation logic.\n",
+ " retry_strategy (RetryStrategy): The retry strategy to use.\n",
+ " Possible keys:\n",
+ " - max_attempts: The maximum number of attempts to make.\n",
+ " - fallback: The LLM or function to use in case of validation failure.\n",
+ " - aggregate_messages: A function to aggregate the messages over multiple turns.\n",
+ " Defaults to fetching the last AI message.\n",
+ " tool_choice: If provided, always run the validator on the tool output.\n",
+ "\n",
+ " Returns:\n",
+ " Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n",
+ " \"\"\"\n",
+ " bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n",
+ " retry_strategy = RetryStrategy(max_attempts=max_attempts)\n",
+ " validator = ValidationNode(tools)\n",
+ " return _bind_validator_with_retries(\n",
+ " bound_llm,\n",
+ " validator=validator,\n",
+ " tool_choice=tool_choice,\n",
+ " retry_strategy=retry_strategy,\n",
+ " ).with_config(metadata={\"retry_strategy\": \"default\"})"
+ ]
},
{
"cell_type": "markdown",
@@ -82,7 +383,27 @@
"id": "5df33c17-ee1a-409e-b5ec-f24e116da7d1",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.pydantic_v1 import BaseModel, Field, validator\n\n\nclass Respond(BaseModel):\n \"\"\"Use to generate the response. Always use when responding to the user\"\"\"\n\n reason: str = Field(description=\"Step-by-step justification for the answer.\")\n answer: str\n\n @validator(\"answer\")\n def reason_contains_apology(cls, answer: str):\n if \"llama\" not in answer.lower():\n raise ValueError(\n \"You MUST start with a gimicky, rhyming advertisement for using a Llama V3 (an LLM) in your **answer** field.\"\n \" Must be an instant hit. Must be weaved into the answer.\"\n )\n\n\ntools = [Respond]"]
+ "source": [
+ "from langchain_core.pydantic_v1 import BaseModel, Field, validator\n",
+ "\n",
+ "\n",
+ "class Respond(BaseModel):\n",
+ " \"\"\"Use to generate the response. Always use when responding to the user\"\"\"\n",
+ "\n",
+ " reason: str = Field(description=\"Step-by-step justification for the answer.\")\n",
+ " answer: str\n",
+ "\n",
+ " @validator(\"answer\")\n",
+ " def reason_contains_apology(cls, answer: str):\n",
+ " if \"llama\" not in answer.lower():\n",
+ " raise ValueError(\n",
+ " \"You MUST start with a gimicky, rhyming advertisement for using a Llama V3 (an LLM) in your **answer** field.\"\n",
+ " \" Must be an instant hit. Must be weaved into the answer.\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "tools = [Respond]"
+ ]
},
{
"cell_type": "markdown",
@@ -98,7 +419,23 @@
"id": "38231a5b-d018-41ee-a92c-2f2248edf417",
"metadata": {},
"outputs": [],
- "source": ["from langchain_anthropic import ChatAnthropic\nfrom langchain_core.prompts import ChatPromptTemplate\n\n# Or you can use ChatGroq, ChatOpenAI, ChatGoogleGemini, ChatCohere, etc.\n# See https://python.langchain.com/v0.2/docs/integrations/chat/ for more info on tool calling\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nbound_llm = bind_validator_with_retries(llm, tools=tools)\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", \"Respond directly by calling the Respond function.\"),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\nchain = prompt | bound_llm"]
+ "source": [
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "\n",
+ "# Or you can use ChatGroq, ChatOpenAI, ChatGoogleGemini, ChatCohere, etc.\n",
+ "# See https://python.langchain.com/v0.2/docs/integrations/chat/ for more info on tool calling\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "bound_llm = bind_validator_with_retries(llm, tools=tools)\n",
+ "prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", \"Respond directly by calling the Respond function.\"),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "chain = prompt | bound_llm"
+ ]
},
{
"cell_type": "code",
@@ -126,7 +463,10 @@
]
}
],
- "source": ["results = chain.invoke({\"messages\": [(\"user\", \"Does P = NP?\")]})\nresults.pretty_print()"]
+ "source": [
+ "results = chain.invoke({\"messages\": [(\"user\", \"Does P = NP?\")]})\n",
+ "results.pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -146,7 +486,97 @@
"id": "f4f7438b-b6c1-48fd-b70f-185af7a2f64a",
"metadata": {},
"outputs": [],
- "source": ["from typing import List, Optional\n\n\nclass OutputFormat(BaseModel):\n sources: str = Field(\n ...,\n description=\"The raw transcript / span you could cite to justify the choice.\",\n )\n content: str = Field(..., description=\"The chosen value.\")\n\n\nclass Moment(BaseModel):\n quote: str = Field(..., description=\"The relevant quote from the transcript.\")\n description: str = Field(..., description=\"A description of the moment.\")\n expressed_preference: OutputFormat = Field(\n ..., description=\"The preference expressed in the moment.\"\n )\n\n\nclass BackgroundInfo(BaseModel):\n factoid: OutputFormat = Field(\n ..., description=\"Important factoid about the member.\"\n )\n professions: list\n why: str = Field(..., description=\"Why this is important.\")\n\n\nclass KeyMoments(BaseModel):\n topic: str = Field(..., description=\"The topic of the key moments.\")\n happy_moments: List[Moment] = Field(\n ..., description=\"A list of key moments related to the topic.\"\n )\n tense_moments: List[Moment] = Field(\n ..., description=\"Moments where things were a bit tense.\"\n )\n sad_moments: List[Moment] = Field(\n ..., description=\"Moments where things where everyone was downtrodden.\"\n )\n background_info: list[BackgroundInfo]\n moments_summary: str = Field(..., description=\"A summary of the key moments.\")\n\n\nclass Member(BaseModel):\n name: OutputFormat = Field(..., description=\"The name of the member.\")\n role: Optional[str] = Field(None, description=\"The role of the member.\")\n age: Optional[int] = Field(None, description=\"The age of the member.\")\n background_details: List[BackgroundInfo] = Field(\n ..., description=\"A list of background details about the member.\"\n )\n\n\nclass InsightfulQuote(BaseModel):\n quote: OutputFormat = Field(\n ..., description=\"An insightful quote from the transcript.\"\n )\n speaker: str = Field(..., description=\"The name of the speaker who said the quote.\")\n analysis: str = Field(\n ..., description=\"An analysis of the quote and its significance.\"\n )\n\n\nclass TranscriptMetadata(BaseModel):\n title: str = Field(..., description=\"The title of the transcript.\")\n location: OutputFormat = Field(\n ..., description=\"The location where the interview took place.\"\n )\n duration: str = Field(..., description=\"The duration of the interview.\")\n\n\nclass TranscriptSummary(BaseModel):\n metadata: TranscriptMetadata = Field(\n ..., description=\"Metadata about the transcript.\"\n )\n participants: List[Member] = Field(\n ..., description=\"A list of participants in the interview.\"\n )\n key_moments: List[KeyMoments] = Field(\n ..., description=\"A list of key moments from the interview.\"\n )\n insightful_quotes: List[InsightfulQuote] = Field(\n ..., description=\"A list of insightful quotes from the interview.\"\n )\n overall_summary: str = Field(\n ..., description=\"An overall summary of the interview.\"\n )\n next_steps: List[str] = Field(\n ..., description=\"A list of next steps or action items based on the interview.\"\n )\n other_stuff: List[OutputFormat]"]
+ "source": [
+ "from typing import List, Optional\n",
+ "\n",
+ "\n",
+ "class OutputFormat(BaseModel):\n",
+ " sources: str = Field(\n",
+ " ...,\n",
+ " description=\"The raw transcript / span you could cite to justify the choice.\",\n",
+ " )\n",
+ " content: str = Field(..., description=\"The chosen value.\")\n",
+ "\n",
+ "\n",
+ "class Moment(BaseModel):\n",
+ " quote: str = Field(..., description=\"The relevant quote from the transcript.\")\n",
+ " description: str = Field(..., description=\"A description of the moment.\")\n",
+ " expressed_preference: OutputFormat = Field(\n",
+ " ..., description=\"The preference expressed in the moment.\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "class BackgroundInfo(BaseModel):\n",
+ " factoid: OutputFormat = Field(\n",
+ " ..., description=\"Important factoid about the member.\"\n",
+ " )\n",
+ " professions: list\n",
+ " why: str = Field(..., description=\"Why this is important.\")\n",
+ "\n",
+ "\n",
+ "class KeyMoments(BaseModel):\n",
+ " topic: str = Field(..., description=\"The topic of the key moments.\")\n",
+ " happy_moments: List[Moment] = Field(\n",
+ " ..., description=\"A list of key moments related to the topic.\"\n",
+ " )\n",
+ " tense_moments: List[Moment] = Field(\n",
+ " ..., description=\"Moments where things were a bit tense.\"\n",
+ " )\n",
+ " sad_moments: List[Moment] = Field(\n",
+ " ..., description=\"Moments where things where everyone was downtrodden.\"\n",
+ " )\n",
+ " background_info: list[BackgroundInfo]\n",
+ " moments_summary: str = Field(..., description=\"A summary of the key moments.\")\n",
+ "\n",
+ "\n",
+ "class Member(BaseModel):\n",
+ " name: OutputFormat = Field(..., description=\"The name of the member.\")\n",
+ " role: Optional[str] = Field(None, description=\"The role of the member.\")\n",
+ " age: Optional[int] = Field(None, description=\"The age of the member.\")\n",
+ " background_details: List[BackgroundInfo] = Field(\n",
+ " ..., description=\"A list of background details about the member.\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "class InsightfulQuote(BaseModel):\n",
+ " quote: OutputFormat = Field(\n",
+ " ..., description=\"An insightful quote from the transcript.\"\n",
+ " )\n",
+ " speaker: str = Field(..., description=\"The name of the speaker who said the quote.\")\n",
+ " analysis: str = Field(\n",
+ " ..., description=\"An analysis of the quote and its significance.\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "class TranscriptMetadata(BaseModel):\n",
+ " title: str = Field(..., description=\"The title of the transcript.\")\n",
+ " location: OutputFormat = Field(\n",
+ " ..., description=\"The location where the interview took place.\"\n",
+ " )\n",
+ " duration: str = Field(..., description=\"The duration of the interview.\")\n",
+ "\n",
+ "\n",
+ "class TranscriptSummary(BaseModel):\n",
+ " metadata: TranscriptMetadata = Field(\n",
+ " ..., description=\"Metadata about the transcript.\"\n",
+ " )\n",
+ " participants: List[Member] = Field(\n",
+ " ..., description=\"A list of participants in the interview.\"\n",
+ " )\n",
+ " key_moments: List[KeyMoments] = Field(\n",
+ " ..., description=\"A list of key moments from the interview.\"\n",
+ " )\n",
+ " insightful_quotes: List[InsightfulQuote] = Field(\n",
+ " ..., description=\"A list of insightful quotes from the interview.\"\n",
+ " )\n",
+ " overall_summary: str = Field(\n",
+ " ..., description=\"An overall summary of the interview.\"\n",
+ " )\n",
+ " next_steps: List[str] = Field(\n",
+ " ..., description=\"A list of next steps or action items based on the interview.\"\n",
+ " )\n",
+ " other_stuff: List[OutputFormat]"
+ ]
},
{
"cell_type": "markdown",
@@ -162,7 +592,82 @@
"id": "e2d10886-7b1e-485f-91cd-1184a1c99303",
"metadata": {},
"outputs": [],
- "source": ["transcript = [\n (\n \"Pete\",\n \"Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.\",\n ),\n (\n \"Xu\",\n \"No problem. As its my job, I've got some thoughts on this beef.\",\n ),\n (\n \"Laura\",\n \"Yeah, I've got some insider info so this should be interesting.\",\n ),\n (\"Pete\", \"Dope. So, when do you think this whole thing started?\"),\n (\n \"Pete\",\n \"Definitely was Kendrick's 'Control' verse that kicked it off.\",\n ),\n (\n \"Laura\",\n \"Truth, but Drake never went after him directly. Just some subtle jabs here and there.\",\n ),\n (\n \"Xu\",\n \"That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.\",\n ),\n (\n \"Pete\",\n \"For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.\",\n ),\n (\n \"Laura\",\n \"I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.\",\n ),\n (\n \"Pete\",\n \"I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.\",\n ),\n (\n \"Xu\",\n \"It's wild how this beef is shaping fans.\",\n ),\n (\"Pete\", \"do you think these beefs can actually be good for hip-hop?\"),\n (\n \"Xu\",\n \"Hell yeah, Pete. When it's done right, a beef can push the genre forward and make artists level up.\",\n ),\n (\"Laura\", \"eh\"),\n (\"Pete\", \"So, where do you see this beef going?\"),\n (\n \"Laura\",\n \"Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate.\",\n ),\n (\"Laura\", \"ehhhhhh not sure\"),\n (\n \"Pete\",\n \"I feel that. I just want both of them to keep dropping heat, beef or no beef.\",\n ),\n (\n \"Xu\",\n \"I'm curious. May influence a lot of people. Make things more competitive. Bring on a whole new wave of lyricism.\",\n ),\n (\n \"Pete\",\n \"Word. Hey, thanks for chopping it up with me, Xu and Laura. This was dope.\",\n ),\n (\"Xu\", \"Where are you going so fast?\"),\n (\n \"Laura\",\n \"For real, I had a good time. Nice to get different perspectives on the situation.\",\n ),\n]\n\nformatted = \"\\n\".join(f\"{x[0]}: {x[1]}\" for x in transcript)"]
+ "source": [
+ "transcript = [\n",
+ " (\n",
+ " \"Pete\",\n",
+ " \"Hey Xu, Laura, thanks for hopping on this call. I've been itching to talk about this Drake and Kendrick situation.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Xu\",\n",
+ " \"No problem. As its my job, I've got some thoughts on this beef.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Laura\",\n",
+ " \"Yeah, I've got some insider info so this should be interesting.\",\n",
+ " ),\n",
+ " (\"Pete\", \"Dope. So, when do you think this whole thing started?\"),\n",
+ " (\n",
+ " \"Pete\",\n",
+ " \"Definitely was Kendrick's 'Control' verse that kicked it off.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Laura\",\n",
+ " \"Truth, but Drake never went after him directly. Just some subtle jabs here and there.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Xu\",\n",
+ " \"That's the thing with beefs like this, though. They've always been a a thing, pushing artists to step up their game.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Pete\",\n",
+ " \"For sure, and this beef has got the fans taking sides. Some are all about Drake's mainstream appeal, while others are digging Kendrick's lyrical skills.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Laura\",\n",
+ " \"I mean, Drake knows how to make a hit that gets everyone hyped. That's his thing.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Pete\",\n",
+ " \"I hear you, Laura, but I gotta give it to Kendrick when it comes to straight-up bars. The man's a beast on the mic.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Xu\",\n",
+ " \"It's wild how this beef is shaping fans.\",\n",
+ " ),\n",
+ " (\"Pete\", \"do you think these beefs can actually be good for hip-hop?\"),\n",
+ " (\n",
+ " \"Xu\",\n",
+ " \"Hell yeah, Pete. When it's done right, a beef can push the genre forward and make artists level up.\",\n",
+ " ),\n",
+ " (\"Laura\", \"eh\"),\n",
+ " (\"Pete\", \"So, where do you see this beef going?\"),\n",
+ " (\n",
+ " \"Laura\",\n",
+ " \"Honestly, I think it'll stay a hot topic for the fans, but unless someone drops a straight-up diss track, it's not gonna escalate.\",\n",
+ " ),\n",
+ " (\"Laura\", \"ehhhhhh not sure\"),\n",
+ " (\n",
+ " \"Pete\",\n",
+ " \"I feel that. I just want both of them to keep dropping heat, beef or no beef.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Xu\",\n",
+ " \"I'm curious. May influence a lot of people. Make things more competitive. Bring on a whole new wave of lyricism.\",\n",
+ " ),\n",
+ " (\n",
+ " \"Pete\",\n",
+ " \"Word. Hey, thanks for chopping it up with me, Xu and Laura. This was dope.\",\n",
+ " ),\n",
+ " (\"Xu\", \"Where are you going so fast?\"),\n",
+ " (\n",
+ " \"Laura\",\n",
+ " \"For real, I had a good time. Nice to get different perspectives on the situation.\",\n",
+ " ),\n",
+ "]\n",
+ "\n",
+ "formatted = \"\\n\".join(f\"{x[0]}: {x[1]}\" for x in transcript)"
+ ]
},
{
"cell_type": "markdown",
@@ -205,7 +710,34 @@
]
}
],
- "source": ["tools = [TranscriptSummary]\nbound_llm = bind_validator_with_retries(\n llm,\n tools=tools,\n)\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", \"Respond directly using the TranscriptSummary function.\"),\n (\"placeholder\", \"{messages}\"),\n ]\n)\n\nchain = prompt | bound_llm\n\nresults = chain.invoke(\n {\n \"messages\": [\n (\n \"user\",\n f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\"\n \"\\n\\nRemember to respond using the TranscriptSummary function.\",\n )\n ]\n },\n)\nresults.pretty_print()"]
+ "source": [
+ "tools = [TranscriptSummary]\n",
+ "bound_llm = bind_validator_with_retries(\n",
+ " llm,\n",
+ " tools=tools,\n",
+ ")\n",
+ "prompt = ChatPromptTemplate.from_messages(\n",
+ " [\n",
+ " (\"system\", \"Respond directly using the TranscriptSummary function.\"),\n",
+ " (\"placeholder\", \"{messages}\"),\n",
+ " ]\n",
+ ")\n",
+ "\n",
+ "chain = prompt | bound_llm\n",
+ "\n",
+ "results = chain.invoke(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " (\n",
+ " \"user\",\n",
+ " f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\"\n",
+ " \"\\n\\nRemember to respond using the TranscriptSummary function.\",\n",
+ " )\n",
+ " ]\n",
+ " },\n",
+ ")\n",
+ "results.pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -233,7 +765,10 @@
"id": "49344104-3ffa-4c66-97fc-5b093a621f70",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U jsonpatch"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U jsonpatch"
+ ]
},
{
"cell_type": "code",
@@ -241,7 +776,150 @@
"id": "af3d5543-1fd4-4e54-b0f9-f1ab42773cfb",
"metadata": {},
"outputs": [],
- "source": ["import logging\n\nlogger = logging.getLogger(\"extraction\")\n\n\ndef bind_validator_with_jsonpatch_retries(\n llm: BaseChatModel,\n *,\n tools: list,\n tool_choice: Optional[str] = None,\n max_attempts: int = 3,\n) -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n\n This method is similar to `bind_validator_with_retries`, but uses JSONPatch to correct\n validation errors caused by passing in incorrect or incomplete parameters in a previous\n tool call. This method requires the 'jsonpatch' library to be installed.\n\n Using patch-based function healing can be more efficient than repopulating the entire\n tool call from scratch, and it can be an easier task for the LLM to perform, since it typically\n only requires a few small changes to the existing tool call.\n\n Args:\n llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n tools (list): The tools to bind to the LLM.\n tool_choice (Optional[str]): The tool choice to use.\n max_attempts (int): The number of attempts to make.\n\n Returns:\n Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n \"\"\"\n\n try:\n import jsonpatch # type: ignore[import-untyped]\n except ImportError:\n raise ImportError(\n \"The 'jsonpatch' library is required for JSONPatch-based retries.\"\n \" Please install it with 'pip install -U jsonpatch'.\"\n )\n\n class JsonPatch(BaseModel):\n \"\"\"A JSON Patch document represents an operation to be performed on a JSON document.\n\n Note that the op and path are ALWAYS required. Value is required for ALL operations except 'remove'.\n Examples:\n\n ```json\n {\"op\": \"add\", \"path\": \"/a/b/c\", \"patch_value\": 1}\n {\"op\": \"replace\", \"path\": \"/a/b/c\", \"patch_value\": 2}\n {\"op\": \"remove\", \"path\": \"/a/b/c\"}\n ```\n \"\"\"\n\n op: Literal[\"add\", \"remove\", \"replace\"] = Field(\n ...,\n description=\"The operation to be performed. Must be one of 'add', 'remove', 'replace'.\",\n )\n path: str = Field(\n ...,\n description=\"A JSON Pointer path that references a location within the target document where the operation is performed.\",\n )\n value: Any = Field(\n ...,\n description=\"The value to be used within the operation. REQUIRED for 'add', 'replace', and 'test' operations.\",\n )\n\n class PatchFunctionParameters(BaseModel):\n \"\"\"Respond with all JSONPatch operation to correct validation errors caused by passing in incorrect or incomplete parameters in a previous tool call.\"\"\"\n\n tool_call_id: str = Field(\n ...,\n description=\"The ID of the original tool call that generated the error. Must NOT be an ID of a PatchFunctionParameters tool call.\",\n )\n reasoning: str = Field(\n ...,\n description=\"Think step-by-step, listing each validation error and the\"\n \" JSONPatch operation needed to correct it. \"\n \"Cite the fields in the JSONSchema you referenced in developing this plan.\",\n )\n patches: list[JsonPatch] = Field(\n ...,\n description=\"A list of JSONPatch operations to be applied to the previous tool call's response.\",\n )\n\n bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n fallback_llm = llm.bind_tools([PatchFunctionParameters])\n\n def aggregate_messages(messages: Sequence[AnyMessage]) -> AIMessage:\n # Get all the AI messages and apply json patches\n resolved_tool_calls: Dict[Union[str, None], ToolCall] = {}\n content: Union[str, List[Union[str, dict]]] = \"\"\n for m in messages:\n if m.type != \"ai\":\n continue\n if not content:\n content = m.content\n for tc in m.tool_calls:\n if tc[\"name\"] == PatchFunctionParameters.__name__:\n tcid = tc[\"args\"][\"tool_call_id\"]\n if tcid not in resolved_tool_calls:\n logger.debug(\n f\"JsonPatch tool call ID {tc['args']['tool_call_id']} not found.\"\n f\"Valid tool call IDs: {list(resolved_tool_calls.keys())}\"\n )\n tcid = next(iter(resolved_tool_calls.keys()), None)\n orig_tool_call = resolved_tool_calls[tcid]\n current_args = orig_tool_call[\"args\"]\n patches = tc[\"args\"].get(\"patches\") or []\n orig_tool_call[\"args\"] = jsonpatch.apply_patch(\n current_args,\n patches,\n )\n orig_tool_call[\"id\"] = tc[\"id\"]\n else:\n resolved_tool_calls[tc[\"id\"]] = tc.copy()\n return AIMessage(\n content=content,\n tool_calls=list(resolved_tool_calls.values()),\n )\n\n def format_exception(error: BaseException, call: ToolCall, schema: Type[BaseModel]):\n return (\n f\"Error:\\n\\n```\\n{repr(error)}\\n```\\n\"\n \"Expected Parameter Schema:\\n\\n\" + f\"```json\\n{schema.schema_json()}\\n```\\n\"\n f\"Please respond with a JSONPatch to correct the error for tool_call_id=[{call['id']}].\"\n )\n\n validator = ValidationNode(\n tools + [PatchFunctionParameters],\n format_error=format_exception,\n )\n retry_strategy = RetryStrategy(\n max_attempts=max_attempts,\n fallback=fallback_llm,\n aggregate_messages=aggregate_messages,\n )\n return _bind_validator_with_retries(\n bound_llm,\n validator=validator,\n retry_strategy=retry_strategy,\n tool_choice=tool_choice,\n ).with_config(metadata={\"retry_strategy\": \"jsonpatch\"})"]
+ "source": [
+ "import logging\n",
+ "\n",
+ "logger = logging.getLogger(\"extraction\")\n",
+ "\n",
+ "\n",
+ "def bind_validator_with_jsonpatch_retries(\n",
+ " llm: BaseChatModel,\n",
+ " *,\n",
+ " tools: list,\n",
+ " tool_choice: Optional[str] = None,\n",
+ " max_attempts: int = 3,\n",
+ ") -> Runnable[Union[List[AnyMessage], PromptValue], AIMessage]:\n",
+ " \"\"\"Binds validators + retry logic ensure validity of generated tool calls.\n",
+ "\n",
+ " This method is similar to `bind_validator_with_retries`, but uses JSONPatch to correct\n",
+ " validation errors caused by passing in incorrect or incomplete parameters in a previous\n",
+ " tool call. This method requires the 'jsonpatch' library to be installed.\n",
+ "\n",
+ " Using patch-based function healing can be more efficient than repopulating the entire\n",
+ " tool call from scratch, and it can be an easier task for the LLM to perform, since it typically\n",
+ " only requires a few small changes to the existing tool call.\n",
+ "\n",
+ " Args:\n",
+ " llm (Runnable): The llm that will generate the initial messages (and optionally fallba)\n",
+ " tools (list): The tools to bind to the LLM.\n",
+ " tool_choice (Optional[str]): The tool choice to use.\n",
+ " max_attempts (int): The number of attempts to make.\n",
+ "\n",
+ " Returns:\n",
+ " Runnable: A runnable that can be invoked with a list of messages and returns a single AI message.\n",
+ " \"\"\"\n",
+ "\n",
+ " try:\n",
+ " import jsonpatch # type: ignore[import-untyped]\n",
+ " except ImportError:\n",
+ " raise ImportError(\n",
+ " \"The 'jsonpatch' library is required for JSONPatch-based retries.\"\n",
+ " \" Please install it with 'pip install -U jsonpatch'.\"\n",
+ " )\n",
+ "\n",
+ " class JsonPatch(BaseModel):\n",
+ " \"\"\"A JSON Patch document represents an operation to be performed on a JSON document.\n",
+ "\n",
+ " Note that the op and path are ALWAYS required. Value is required for ALL operations except 'remove'.\n",
+ " Examples:\n",
+ "\n",
+ " ```json\n",
+ " {\"op\": \"add\", \"path\": \"/a/b/c\", \"patch_value\": 1}\n",
+ " {\"op\": \"replace\", \"path\": \"/a/b/c\", \"patch_value\": 2}\n",
+ " {\"op\": \"remove\", \"path\": \"/a/b/c\"}\n",
+ " ```\n",
+ " \"\"\"\n",
+ "\n",
+ " op: Literal[\"add\", \"remove\", \"replace\"] = Field(\n",
+ " ...,\n",
+ " description=\"The operation to be performed. Must be one of 'add', 'remove', 'replace'.\",\n",
+ " )\n",
+ " path: str = Field(\n",
+ " ...,\n",
+ " description=\"A JSON Pointer path that references a location within the target document where the operation is performed.\",\n",
+ " )\n",
+ " value: Any = Field(\n",
+ " ...,\n",
+ " description=\"The value to be used within the operation. REQUIRED for 'add', 'replace', and 'test' operations.\",\n",
+ " )\n",
+ "\n",
+ " class PatchFunctionParameters(BaseModel):\n",
+ " \"\"\"Respond with all JSONPatch operation to correct validation errors caused by passing in incorrect or incomplete parameters in a previous tool call.\"\"\"\n",
+ "\n",
+ " tool_call_id: str = Field(\n",
+ " ...,\n",
+ " description=\"The ID of the original tool call that generated the error. Must NOT be an ID of a PatchFunctionParameters tool call.\",\n",
+ " )\n",
+ " reasoning: str = Field(\n",
+ " ...,\n",
+ " description=\"Think step-by-step, listing each validation error and the\"\n",
+ " \" JSONPatch operation needed to correct it. \"\n",
+ " \"Cite the fields in the JSONSchema you referenced in developing this plan.\",\n",
+ " )\n",
+ " patches: list[JsonPatch] = Field(\n",
+ " ...,\n",
+ " description=\"A list of JSONPatch operations to be applied to the previous tool call's response.\",\n",
+ " )\n",
+ "\n",
+ " bound_llm = llm.bind_tools(tools, tool_choice=tool_choice)\n",
+ " fallback_llm = llm.bind_tools([PatchFunctionParameters])\n",
+ "\n",
+ " def aggregate_messages(messages: Sequence[AnyMessage]) -> AIMessage:\n",
+ " # Get all the AI messages and apply json patches\n",
+ " resolved_tool_calls: Dict[Union[str, None], ToolCall] = {}\n",
+ " content: Union[str, List[Union[str, dict]]] = \"\"\n",
+ " for m in messages:\n",
+ " if m.type != \"ai\":\n",
+ " continue\n",
+ " if not content:\n",
+ " content = m.content\n",
+ " for tc in m.tool_calls:\n",
+ " if tc[\"name\"] == PatchFunctionParameters.__name__:\n",
+ " tcid = tc[\"args\"][\"tool_call_id\"]\n",
+ " if tcid not in resolved_tool_calls:\n",
+ " logger.debug(\n",
+ " f\"JsonPatch tool call ID {tc['args']['tool_call_id']} not found.\"\n",
+ " f\"Valid tool call IDs: {list(resolved_tool_calls.keys())}\"\n",
+ " )\n",
+ " tcid = next(iter(resolved_tool_calls.keys()), None)\n",
+ " orig_tool_call = resolved_tool_calls[tcid]\n",
+ " current_args = orig_tool_call[\"args\"]\n",
+ " patches = tc[\"args\"].get(\"patches\") or []\n",
+ " orig_tool_call[\"args\"] = jsonpatch.apply_patch(\n",
+ " current_args,\n",
+ " patches,\n",
+ " )\n",
+ " orig_tool_call[\"id\"] = tc[\"id\"]\n",
+ " else:\n",
+ " resolved_tool_calls[tc[\"id\"]] = tc.copy()\n",
+ " return AIMessage(\n",
+ " content=content,\n",
+ " tool_calls=list(resolved_tool_calls.values()),\n",
+ " )\n",
+ "\n",
+ " def format_exception(error: BaseException, call: ToolCall, schema: Type[BaseModel]):\n",
+ " return (\n",
+ " f\"Error:\\n\\n```\\n{repr(error)}\\n```\\n\"\n",
+ " \"Expected Parameter Schema:\\n\\n\" + f\"```json\\n{schema.schema_json()}\\n```\\n\"\n",
+ " f\"Please respond with a JSONPatch to correct the error for tool_call_id=[{call['id']}].\"\n",
+ " )\n",
+ "\n",
+ " validator = ValidationNode(\n",
+ " tools + [PatchFunctionParameters],\n",
+ " format_error=format_exception,\n",
+ " )\n",
+ " retry_strategy = RetryStrategy(\n",
+ " max_attempts=max_attempts,\n",
+ " fallback=fallback_llm,\n",
+ " aggregate_messages=aggregate_messages,\n",
+ " )\n",
+ " return _bind_validator_with_retries(\n",
+ " bound_llm,\n",
+ " validator=validator,\n",
+ " retry_strategy=retry_strategy,\n",
+ " tool_choice=tool_choice,\n",
+ " ).with_config(metadata={\"retry_strategy\": \"jsonpatch\"})"
+ ]
},
{
"cell_type": "code",
@@ -249,7 +927,9 @@
"id": "b01891c4-4187-4a75-9eda-644a7c2355f3",
"metadata": {},
"outputs": [],
- "source": ["bound_llm = bind_validator_with_jsonpatch_retries(llm, tools=tools)"]
+ "source": [
+ "bound_llm = bind_validator_with_jsonpatch_retries(llm, tools=tools)"
+ ]
},
{
"cell_type": "code",
@@ -268,7 +948,14 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(bound_llm.get_graph().draw_mermaid_png()))\nexcept Exception:\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(bound_llm.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " pass"
+ ]
},
{
"cell_type": "code",
@@ -297,7 +984,20 @@
]
}
],
- "source": ["chain = prompt | bound_llm\nresults = chain.invoke(\n {\n \"messages\": [\n (\n \"user\",\n f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\",\n ),\n ]\n },\n)\nresults.pretty_print()"]
+ "source": [
+ "chain = prompt | bound_llm\n",
+ "results = chain.invoke(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " (\n",
+ " \"user\",\n",
+ " f\"Extract the summary from the following conversation:\\n\\n\\n{formatted}\\n\",\n",
+ " ),\n",
+ " ]\n",
+ " },\n",
+ ")\n",
+ "results.pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -317,7 +1017,7 @@
"id": "0ae295b1-da58-4cc9-834b-70e1466f8695",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": []
}
],
"metadata": {
@@ -336,7 +1036,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.1"
+ "version": "3.12.2"
}
},
"nbformat": 4,
diff --git a/examples/human_in_the_loop/breakpoints.ipynb b/examples/human_in_the_loop/breakpoints.ipynb
index 868ea9b20..cd03657dd 100644
--- a/examples/human_in_the_loop/breakpoints.ipynb
+++ b/examples/human_in_the_loop/breakpoints.ipynb
@@ -32,7 +32,9 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
+ "source": [
+ "%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"
+ ]
},
{
"cell_type": "markdown",
@@ -56,7 +58,9 @@
]
}
],
- "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\")"]
+ "source": [
+ "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"
+ ]
},
{
"cell_type": "markdown",
@@ -72,7 +76,9 @@
"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",
@@ -90,7 +96,105 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
- "source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
+ "source": [
+ "# Set up the tool\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_core.tools import tool\n",
+ "from langgraph.graph import MessagesState, START\n",
+ "from langgraph.prebuilt import ToolNode\n",
+ "from langgraph.graph import END, StateGraph\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def search(query: str):\n",
+ " \"\"\"Call to surf the web.\"\"\"\n",
+ " # This is a placeholder for the actual implementation\n",
+ " # Don't let the LLM know this though 😊\n",
+ " return [\n",
+ " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "tools = [search]\n",
+ "tool_node = ToolNode(tools)\n",
+ "\n",
+ "# Set up the model\n",
+ "\n",
+ "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
+ "model = model.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "# Define nodes and conditional edges\n",
+ "\n",
+ "\n",
+ "# Define the function that determines whether to continue or not\n",
+ "def should_continue(state):\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ " # If there is no function call, then we finish\n",
+ " if not last_message.tool_calls:\n",
+ " return \"end\"\n",
+ " # Otherwise if there is, we continue\n",
+ " else:\n",
+ " return \"continue\"\n",
+ "\n",
+ "\n",
+ "# Define the function that calls the model\n",
+ "def call_model(state):\n",
+ " messages = state[\"messages\"]\n",
+ " response = model.invoke(messages)\n",
+ " # We return a list, because this will get added to the existing list\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(MessagesState)\n",
+ "\n",
+ "# Define the two nodes we will cycle between\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"action\", tool_node)\n",
+ "\n",
+ "# Set the entrypoint as `agent`\n",
+ "# This means that this node is the first one called\n",
+ "workflow.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\")\n",
+ "\n",
+ "# Set up memory\n",
+ "memory = MemorySaver()\n",
+ "\n",
+ "# Finally, we compile it!\n",
+ "# This compiles it into a LangChain Runnable,\n",
+ "# meaning you can use it as you would any other runnable\n",
+ "\n",
+ "# We add in `interrupt_before=[\"action\"]`\n",
+ "# This will add a breakpoint before the `action` node is called\n",
+ "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -126,7 +230,9 @@
]
}
],
- "source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -166,7 +272,9 @@
]
}
],
- "source": ["for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "for event in app.stream(None, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"
+ ]
}
],
"metadata": {
diff --git a/examples/human_in_the_loop/edit-graph-state.ipynb b/examples/human_in_the_loop/edit-graph-state.ipynb
index 145940a33..b432cd84d 100644
--- a/examples/human_in_the_loop/edit-graph-state.ipynb
+++ b/examples/human_in_the_loop/edit-graph-state.ipynb
@@ -32,7 +32,10 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install --quiet -U langgraph langchain_anthropic"
+ ]
},
{
"cell_type": "markdown",
@@ -49,14 +52,25 @@
"metadata": {},
"outputs": [
{
- "name": "stdin",
+ "name": "stdout",
"output_type": "stream",
"text": [
"ANTHROPIC_API_KEY: ········\n"
]
}
],
- "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\")"]
+ "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\")"
+ ]
},
{
"cell_type": "markdown",
@@ -72,7 +86,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",
@@ -90,7 +107,104 @@
"id": "6098e5cb",
"metadata": {},
"outputs": [],
- "source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
+ "source": [
+ "# Set up the tool\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_core.tools import tool\n",
+ "from langgraph.graph import MessagesState, START, END, StateGraph\n",
+ "from langgraph.prebuilt import ToolNode\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def search(query: str):\n",
+ " \"\"\"Call to surf the web.\"\"\"\n",
+ " # This is a placeholder for the actual implementation\n",
+ " # Don't let the LLM know this though 😊\n",
+ " return [\n",
+ " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "tools = [search]\n",
+ "tool_node = ToolNode(tools)\n",
+ "\n",
+ "# Set up the model\n",
+ "\n",
+ "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
+ "model = model.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "# Define nodes and conditional edges\n",
+ "\n",
+ "\n",
+ "# Define the function that determines whether to continue or not\n",
+ "def should_continue(state):\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ " # If there is no function call, then we finish\n",
+ " if not last_message.tool_calls:\n",
+ " return \"end\"\n",
+ " # Otherwise if there is, we continue\n",
+ " else:\n",
+ " return \"continue\"\n",
+ "\n",
+ "\n",
+ "# Define the function that calls the model\n",
+ "def call_model(state):\n",
+ " messages = state[\"messages\"]\n",
+ " response = model.invoke(messages)\n",
+ " # We return a list, because this will get added to the existing list\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(MessagesState)\n",
+ "\n",
+ "# Define the two nodes we will cycle between\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"action\", tool_node)\n",
+ "\n",
+ "# Set the entrypoint as `agent`\n",
+ "# This means that this node is the first one called\n",
+ "workflow.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\")\n",
+ "\n",
+ "# Set up memory\n",
+ "memory = MemorySaver()\n",
+ "\n",
+ "# Finally, we compile it!\n",
+ "# This compiles it into a LangChain Runnable,\n",
+ "# meaning you can use it as you would any other runnable\n",
+ "\n",
+ "# We add in `interrupt_before=[\"action\"]`\n",
+ "# This will add a breakpoint before the `action` node is called\n",
+ "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -126,7 +240,14 @@
]
}
],
- "source": ["from langchain_core.messages import HumanMessage\n\nthread = {\"configurable\": {\"thread_id\": \"3\"}}\ninputs = [HumanMessage(content=\"search for the weather in sf now\")]\nfor event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "from langchain_core.messages import HumanMessage\n",
+ "\n",
+ "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
+ "inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n",
+ "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -156,7 +277,25 @@
"output_type": "execute_result"
}
],
- "source": ["# First, lets get the current state\ncurrent_state = app.get_state(thread)\n\n# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = current_state.values['messages'][-1]\n\n# Let's now update the args for that tool call\nlast_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n\n# Let's now call `update_state` to pass in this message in the `messages` key\n# This will get treated as any other update to the state\n# It will get passed to the reducer function for the `messages` key\n# That reducer function will use the ID of the message to update it\n# It's important that it has the right ID! Otherwise it would get appended\n# as a new message\napp.update_state(thread, {\"messages\": last_message})"]
+ "source": [
+ "# First, lets get the current state\n",
+ "current_state = app.get_state(thread)\n",
+ "\n",
+ "# Let's now get the last message in the state\n",
+ "# This is the one with the tool calls that we want to update\n",
+ "last_message = current_state.values[\"messages\"][-1]\n",
+ "\n",
+ "# Let's now update the args for that tool call\n",
+ "last_message.tool_calls[0][\"args\"] = {\"query\": \"current weather in SF\"}\n",
+ "\n",
+ "# Let's now call `update_state` to pass in this message in the `messages` key\n",
+ "# This will get treated as any other update to the state\n",
+ "# It will get passed to the reducer function for the `messages` key\n",
+ "# That reducer function will use the ID of the message to update it\n",
+ "# It's important that it has the right ID! Otherwise it would get appended\n",
+ "# as a new message\n",
+ "app.update_state(thread, {\"messages\": last_message})"
+ ]
},
{
"cell_type": "markdown",
@@ -185,7 +324,10 @@
"output_type": "execute_result"
}
],
- "source": ["current_state = app.get_state(thread).values['messages'][-1].tool_calls\ncurrent_state"]
+ "source": [
+ "current_state = app.get_state(thread).values[\"messages\"][-1].tool_calls\n",
+ "current_state"
+ ]
},
{
"cell_type": "markdown",
@@ -223,7 +365,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": "code",
@@ -231,7 +376,7 @@
"id": "78780afe-409d-46cd-a734-e82538cdd8de",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": []
}
],
"metadata": {
diff --git a/examples/human_in_the_loop/time-travel.ipynb b/examples/human_in_the_loop/time-travel.ipynb
index 04c89eec9..fdfd454c6 100644
--- a/examples/human_in_the_loop/time-travel.ipynb
+++ b/examples/human_in_the_loop/time-travel.ipynb
@@ -39,7 +39,9 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
+ "source": [
+ "%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"
+ ]
},
{
"cell_type": "markdown",
@@ -63,7 +65,9 @@
]
}
],
- "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\")"]
+ "source": [
+ "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"
+ ]
},
{
"cell_type": "markdown",
@@ -79,7 +83,9 @@
"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",
@@ -97,7 +103,105 @@
"id": "f5319e01",
"metadata": {},
"outputs": [],
- "source": ["# Set up the tool\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.tools import tool\nfrom langgraph.graph import MessagesState, START\nfrom langgraph.prebuilt import ToolNode\nfrom langgraph.graph import END, StateGraph\nfrom langgraph.checkpoint.memory import MemorySaver\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\nmodel = model.bind_tools(tools)\n\n\n# Define nodes and conditional edges\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Set up memory\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n\n# We add in `interrupt_before=[\"action\"]`\n# This will add a breakpoint before the `action` node is called\napp = workflow.compile(checkpointer=memory)"]
+ "source": [
+ "# Set up the tool\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_core.tools import tool\n",
+ "from langgraph.graph import MessagesState, START\n",
+ "from langgraph.prebuilt import ToolNode\n",
+ "from langgraph.graph import END, StateGraph\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def search(query: str):\n",
+ " \"\"\"Call to surf the web.\"\"\"\n",
+ " # This is a placeholder for the actual implementation\n",
+ " # Don't let the LLM know this though 😊\n",
+ " return [\n",
+ " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "tools = [search]\n",
+ "tool_node = ToolNode(tools)\n",
+ "\n",
+ "# Set up the model\n",
+ "\n",
+ "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
+ "model = model.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "# Define nodes and conditional edges\n",
+ "\n",
+ "\n",
+ "# Define the function that determines whether to continue or not\n",
+ "def should_continue(state):\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ " # If there is no function call, then we finish\n",
+ " if not last_message.tool_calls:\n",
+ " return \"end\"\n",
+ " # Otherwise if there is, we continue\n",
+ " else:\n",
+ " return \"continue\"\n",
+ "\n",
+ "\n",
+ "# Define the function that calls the model\n",
+ "def call_model(state):\n",
+ " messages = state[\"messages\"]\n",
+ " response = model.invoke(messages)\n",
+ " # We return a list, because this will get added to the existing list\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(MessagesState)\n",
+ "\n",
+ "# Define the two nodes we will cycle between\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"action\", tool_node)\n",
+ "\n",
+ "# Set the entrypoint as `agent`\n",
+ "# This means that this node is the first one called\n",
+ "workflow.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\")\n",
+ "\n",
+ "# Set up memory\n",
+ "memory = MemorySaver()\n",
+ "\n",
+ "# Finally, we compile it!\n",
+ "# This compiles it into a LangChain Runnable,\n",
+ "# meaning you can use it as you would any other runnable\n",
+ "\n",
+ "# We add in `interrupt_before=[\"action\"]`\n",
+ "# This will add a breakpoint before the `action` node is called\n",
+ "app = workflow.compile(checkpointer=memory)"
+ ]
},
{
"cell_type": "markdown",
@@ -151,7 +255,9 @@
]
}
],
- "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\ninput_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\ninput_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -186,7 +292,9 @@
]
}
],
- "source": ["all_states = []\nfor state in app.get_state_history(config):\n print(state)\n all_states.append(state)\n print(\"--\")"]
+ "source": [
+ "all_states = []\nfor state in app.get_state_history(config):\n print(state)\n all_states.append(state)\n print(\"--\")"
+ ]
},
{
"cell_type": "markdown",
@@ -204,7 +312,9 @@
"id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699",
"metadata": {},
"outputs": [],
- "source": ["to_replay = all_states[2]"]
+ "source": [
+ "to_replay = all_states[2]"
+ ]
},
{
"cell_type": "code",
@@ -224,7 +334,9 @@
"output_type": "execute_result"
}
],
- "source": ["to_replay.values"]
+ "source": [
+ "to_replay.values"
+ ]
},
{
"cell_type": "code",
@@ -243,7 +355,9 @@
"output_type": "execute_result"
}
],
- "source": ["to_replay.next"]
+ "source": [
+ "to_replay.next"
+ ]
},
{
"cell_type": "markdown",
@@ -268,7 +382,9 @@
]
}
],
- "source": ["for event in app.stream(None, to_replay.config):\n for v in event.values():\n print(v)"]
+ "source": [
+ "for event in app.stream(None, to_replay.config):\n for v in event.values():\n print(v)"
+ ]
},
{
"cell_type": "markdown",
@@ -288,7 +404,19 @@
"id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f",
"metadata": {},
"outputs": [],
- "source": ["# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = to_replay.values['messages'][-1]\n\n# Let's now update the args for that tool call\nlast_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n\nbranch_config = app.update_state(\n to_replay.config, {\"messages\": [last_message]},\n)"]
+ "source": [
+ "# Let's now get the last message in the state\n",
+ "# This is the one with the tool calls that we want to update\n",
+ "last_message = to_replay.values[\"messages\"][-1]\n",
+ "\n",
+ "# Let's now update the args for that tool call\n",
+ "last_message.tool_calls[0][\"args\"] = {\"query\": \"current weather in SF\"}\n",
+ "\n",
+ "branch_config = app.update_state(\n",
+ " to_replay.config,\n",
+ " {\"messages\": [last_message]},\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -313,7 +441,9 @@
]
}
],
- "source": ["for event in app.stream(None, branch_config):\n for v in event.values():\n print(v)"]
+ "source": [
+ "for event in app.stream(None, branch_config):\n for v in event.values():\n print(v)"
+ ]
},
{
"cell_type": "markdown",
@@ -329,7 +459,21 @@
"id": "01abb480-df55-4eba-a2be-cf9372b60b54",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import AIMessage\n\n# Let's now get the last message in the state\n# This is the one with the tool calls that we want to update\nlast_message = to_replay.values['messages'][-1]\n\n# Let's now get the ID for the last message, and create a new message with that ID.\nnew_message = AIMessage(content=\"its warm!\", id=last_message.id)\n\nbranch_config = app.update_state(\n to_replay.config, {\"messages\": [new_message]},\n)"]
+ "source": [
+ "from langchain_core.messages import AIMessage\n",
+ "\n",
+ "# Let's now get the last message in the state\n",
+ "# This is the one with the tool calls that we want to update\n",
+ "last_message = to_replay.values[\"messages\"][-1]\n",
+ "\n",
+ "# Let's now get the ID for the last message, and create a new message with that ID.\n",
+ "new_message = AIMessage(content=\"its warm!\", id=last_message.id)\n",
+ "\n",
+ "branch_config = app.update_state(\n",
+ " to_replay.config,\n",
+ " {\"messages\": [new_message]},\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -337,7 +481,9 @@
"id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641",
"metadata": {},
"outputs": [],
- "source": ["branch_state = app.get_state(branch_config)"]
+ "source": [
+ "branch_state = app.get_state(branch_config)"
+ ]
},
{
"cell_type": "code",
@@ -357,7 +503,9 @@
"output_type": "execute_result"
}
],
- "source": ["branch_state.values"]
+ "source": [
+ "branch_state.values"
+ ]
},
{
"cell_type": "code",
@@ -376,7 +524,9 @@
"output_type": "execute_result"
}
],
- "source": ["branch_state.next"]
+ "source": [
+ "branch_state.next"
+ ]
},
{
"cell_type": "markdown",
@@ -392,7 +542,9 @@
"id": "74a7a5ed-0c14-4883-a16b-d70aaf40f7ea",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb
index 494505a5b..1fbb5fbe2 100644
--- a/examples/human_in_the_loop/wait-user-input.ipynb
+++ b/examples/human_in_the_loop/wait-user-input.ipynb
@@ -40,7 +40,9 @@
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"]
+ "source": [
+ "%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_anthropic"
+ ]
},
{
"cell_type": "markdown",
@@ -64,7 +66,9 @@
]
}
],
- "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\")"]
+ "source": [
+ "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"
+ ]
},
{
"cell_type": "markdown",
@@ -80,7 +84,9 @@
"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",
@@ -98,7 +104,147 @@
"id": "f5319e01",
"metadata": {},
"outputs": [],
- "source": ["# Set up the state\nfrom langgraph.graph import MessagesState, START\n\n# Set up the tool\n# We will have one real tool - a search tool\n# We'll also have one \"fake\" tool - a \"ask_human\" tool\n# Here we define any ACTUAL tools\nfrom langchain_core.tools import tool\nfrom langgraph.prebuilt import ToolNode\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n # Don't let the LLM know this though 😊\n return [\n f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n ]\n\n\ntools = [search]\ntool_node = ToolNode(tools)\n\n# Set up the model\nfrom langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n\n\n# We are going \"bind\" all tools to the model\n# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n# Since `bind_tools` takes in tools but also just tool definitions,\n# We can define a tool definition for `ask_human`\n\nfrom langchain_core.pydantic_v1 import BaseModel\n\nclass AskHuman(BaseModel):\n \"\"\"Ask the human a question\"\"\"\n question: str\n\n\nmodel = model.bind_tools(tools + [AskHuman])\n\n# Define nodes and conditional edges\n\nfrom langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # If tool call is asking Human, we return that node\n # You could also add logic here to let some system know that there's something that requires Human input\n # For example, send a slack message, etc\n elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n return \"ask_human\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state):\n messages = state[\"messages\"]\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n\n# We define a fake node to ask the human\ndef ask_human(state):\n pass\n\n# Build the graph\n\nfrom langgraph.graph import END, StateGraph\n\n# Define a new graph\nworkflow = StateGraph(MessagesState)\n\n# Define the three nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\nworkflow.add_node(\"ask_human\", ask_human)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # We may ask the human\n \"ask_human\": \"ask_human\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# After we get back the human response, we go back to the agent\nworkflow.add_edge(\"ask_human\", \"agent\")\n\n# Set up memory\nfrom langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\n# We add a breakpoint BEFORE the `ask_human` node so it never executes\napp = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])"]
+ "source": [
+ "# Set up the state\n",
+ "from langgraph.graph import MessagesState, START\n",
+ "\n",
+ "# Set up the tool\n",
+ "# We will have one real tool - a search tool\n",
+ "# We'll also have one \"fake\" tool - a \"ask_human\" tool\n",
+ "# Here we define any ACTUAL tools\n",
+ "from langchain_core.tools import tool\n",
+ "from langgraph.prebuilt import ToolNode\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",
+ " f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
+ " ]\n",
+ "\n",
+ "\n",
+ "tools = [search]\n",
+ "tool_node = ToolNode(tools)\n",
+ "\n",
+ "# Set up the model\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
+ "\n",
+ "\n",
+ "# We are going \"bind\" all tools to the model\n",
+ "# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n",
+ "# Since `bind_tools` takes in tools but also just tool definitions,\n",
+ "# We can define a tool definition for `ask_human`\n",
+ "\n",
+ "from langchain_core.pydantic_v1 import BaseModel\n",
+ "\n",
+ "\n",
+ "class AskHuman(BaseModel):\n",
+ " \"\"\"Ask the human a question\"\"\"\n",
+ "\n",
+ " question: str\n",
+ "\n",
+ "\n",
+ "model = model.bind_tools(tools + [AskHuman])\n",
+ "\n",
+ "# Define nodes and conditional edges\n",
+ "\n",
+ "from langchain_core.messages import ToolMessage\n",
+ "\n",
+ "from langgraph.prebuilt import ToolInvocation\n",
+ "\n",
+ "\n",
+ "# Define the function that determines whether to continue or not\n",
+ "def should_continue(state):\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ " # If there is no function call, then we finish\n",
+ " if not last_message.tool_calls:\n",
+ " return \"end\"\n",
+ " # If tool call is asking Human, we return that node\n",
+ " # You could also add logic here to let some system know that there's something that requires Human input\n",
+ " # For example, send a slack message, etc\n",
+ " elif last_message.tool_calls[0][\"name\"] == \"AskHuman\":\n",
+ " return \"ask_human\"\n",
+ " # Otherwise if there is, we continue\n",
+ " else:\n",
+ " return \"continue\"\n",
+ "\n",
+ "\n",
+ "# Define the function that calls the model\n",
+ "def call_model(state):\n",
+ " messages = state[\"messages\"]\n",
+ " response = model.invoke(messages)\n",
+ " # We return a list, because this will get added to the existing list\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "# We define a fake node to ask the human\n",
+ "def ask_human(state):\n",
+ " pass\n",
+ "\n",
+ "\n",
+ "# Build the graph\n",
+ "\n",
+ "from langgraph.graph import END, StateGraph\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(MessagesState)\n",
+ "\n",
+ "# Define the three nodes we will cycle between\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"action\", tool_node)\n",
+ "workflow.add_node(\"ask_human\", ask_human)\n",
+ "\n",
+ "# Set the entrypoint as `agent`\n",
+ "# This means that this node is the first one called\n",
+ "workflow.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",
+ " # We may ask the human\n",
+ " \"ask_human\": \"ask_human\",\n",
+ " # Otherwise we finish.\n",
+ " \"end\": END,\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# We now add a normal edge from `tools` to `agent`.\n",
+ "# This means that after `tools` is called, `agent` node is called next.\n",
+ "workflow.add_edge(\"action\", \"agent\")\n",
+ "\n",
+ "# After we get back the human response, we go back to the agent\n",
+ "workflow.add_edge(\"ask_human\", \"agent\")\n",
+ "\n",
+ "# Set up memory\n",
+ "from langgraph.checkpoint.memory import MemorySaver\n",
+ "\n",
+ "memory = MemorySaver()\n",
+ "\n",
+ "# Finally, we compile it!\n",
+ "# This compiles it into a LangChain Runnable,\n",
+ "# meaning you can use it as you would any other runnable\n",
+ "# We add a breakpoint BEFORE the `ask_human` node so it never executes\n",
+ "app = workflow.compile(checkpointer=memory, interrupt_before=[\"ask_human\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -134,7 +280,16 @@
]
}
],
- "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "from langchain_core.messages import HumanMessage\n",
+ "\n",
+ "config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
+ "input_message = HumanMessage(\n",
+ " content=\"Use the search tool to ask the user where they are, then look up the weather there\"\n",
+ ")\n",
+ "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -163,7 +318,31 @@
"output_type": "execute_result"
}
],
- "source": ["tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n\n# We now create the tool call with the id and the response we want\ntool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n\n# # This is equivalent to the below, either one works\n# from langchain_core.messages import ToolMessage\n# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n\n# We now update the state\n# Notice that we are also specifying `as_node=\"ask_human\"`\n# This will apply this update as this node,\n# which will make it so that afterwards it continues as normal\napp.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n\n# We can check the state\n# We can see that the state currently has the `agent` node next\n# This is based on how we define our graph, \n# where after the `ask_human` node goes (which we just triggered)\n# there is an edge to the `agent` node\napp.get_state(config).next"]
+ "source": [
+ "tool_call_id = app.get_state(config).values[\"messages\"][-1].tool_calls[0][\"id\"]\n",
+ "\n",
+ "# We now create the tool call with the id and the response we want\n",
+ "tool_message = [\n",
+ " {\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}\n",
+ "]\n",
+ "\n",
+ "# # This is equivalent to the below, either one works\n",
+ "# from langchain_core.messages import ToolMessage\n",
+ "# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n",
+ "\n",
+ "# We now update the state\n",
+ "# Notice that we are also specifying `as_node=\"ask_human\"`\n",
+ "# This will apply this update as this node,\n",
+ "# which will make it so that afterwards it continues as normal\n",
+ "app.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n",
+ "\n",
+ "# We can check the state\n",
+ "# We can see that the state currently has the `agent` node next\n",
+ "# This is based on how we define our graph,\n",
+ "# where after the `ask_human` node goes (which we just triggered)\n",
+ "# there is an edge to the `agent` node\n",
+ "app.get_state(config).next"
+ ]
},
{
"cell_type": "markdown",
@@ -205,7 +384,9 @@
]
}
],
- "source": ["for event in app.stream(None, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "for event in app.stream(None, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "code",
@@ -213,7 +394,9 @@
"id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb
index bd4319b65..343c9e014 100644
--- a/examples/introduction.ipynb
+++ b/examples/introduction.ipynb
@@ -28,7 +28,13 @@
"id": "6f11d631-8679-4f28-822f-cdf1f2ddc21c",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langgraph langsmith\n\n# Used for this tutorial; not a requirement for LangGraph\n%pip install -U langchain_anthropic"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph langsmith\n",
+ "\n",
+ "# Used for this tutorial; not a requirement for LangGraph\n",
+ "%pip install -U langchain_anthropic"
+ ]
},
{
"cell_type": "markdown",
@@ -44,7 +50,18 @@
"id": "705d4020-6ee8-44cc-b1a5-8c34e7172fc7",
"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\")"]
+ "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\")"
+ ]
},
{
"cell_type": "markdown",
@@ -60,7 +77,11 @@
"id": "13cba9af-0572-41df-92f8-d6f56d5b5322",
"metadata": {},
"outputs": [],
- "source": ["_set_env(\"LANGSMITH_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"LangGraph Tutorial\""]
+ "source": [
+ "_set_env(\"LANGSMITH_API_KEY\")\n",
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
+ "os.environ[\"LANGCHAIN_PROJECT\"] = \"LangGraph Tutorial\""
+ ]
},
{
"cell_type": "markdown",
@@ -80,7 +101,24 @@
"id": "e58df974-7579-4f25-9d91-66389b94eba2",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n # Messages have the type \"list\". The `add_messages` function\n # in the annotation defines how this state key should be updated\n # (in this case, it appends messages to the list, rather than overwriting them)\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START, END\n",
+ "from langgraph.graph.message import add_messages\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " # Messages have the type \"list\". The `add_messages` function\n",
+ " # in the annotation defines how this state key should be updated\n",
+ " # (in this case, it appends messages to the list, rather than overwriting them)\n",
+ " messages: Annotated[list, add_messages]\n",
+ "\n",
+ "\n",
+ "graph_builder = StateGraph(State)"
+ ]
},
{
"cell_type": "markdown",
@@ -103,7 +141,21 @@
"id": "bc8c9137-8261-42ea-8e83-3590981d23e2",
"metadata": {},
"outputs": [],
- "source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm.invoke(state[\"messages\"])]}\n\n\n# The first argument is the unique node name\n# The second argument is the function or object that will be called whenever\n# the node is used.\ngraph_builder.add_node(\"chatbot\", chatbot)"]
+ "source": [
+ "from langchain_anthropic import ChatAnthropic\n",
+ "\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " return {\"messages\": [llm.invoke(state[\"messages\"])]}\n",
+ "\n",
+ "\n",
+ "# The first argument is the unique node name\n",
+ "# The second argument is the function or object that will be called whenever\n",
+ "# the node is used.\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)"
+ ]
},
{
"cell_type": "markdown",
@@ -123,7 +175,9 @@
"id": "e331e10d-ebcf-4144-9bd3-999b4d656dd3",
"metadata": {},
"outputs": [],
- "source": ["graph_builder.add_edge(START, \"chatbot\")"]
+ "source": [
+ "graph_builder.add_edge(START, \"chatbot\")"
+ ]
},
{
"cell_type": "markdown",
@@ -139,7 +193,9 @@
"id": "075f0929-3591-4852-b2d3-eaadde40662d",
"metadata": {},
"outputs": [],
- "source": ["graph_builder.set_finish_point(\"chatbot\")"]
+ "source": [
+ "graph_builder.add_edge(\"chatbot\", END)"
+ ]
},
{
"cell_type": "markdown",
@@ -155,7 +211,9 @@
"id": "0bb67a01-cf5c-4625-8c07-6e8c0af50fca",
"metadata": {},
"outputs": [],
- "source": ["graph = graph_builder.compile()"]
+ "source": [
+ "graph = graph_builder.compile()"
+ ]
},
{
"cell_type": "markdown",
@@ -182,7 +240,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -257,7 +323,16 @@
]
}
],
- "source": ["while True:\n user_input = input(\"User: \")\n if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n print(\"Goodbye!\")\n break\n for event in graph.stream({\"messages\": (\"user\", user_input)}):\n for value in event.values():\n print(\"Assistant:\", value[\"messages\"][-1].content)"]
+ "source": [
+ "while True:\n",
+ " user_input = input(\"User: \")\n",
+ " if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n",
+ " print(\"Goodbye!\")\n",
+ " break\n",
+ " for event in graph.stream({\"messages\": (\"user\", user_input)}):\n",
+ " for value in event.values():\n",
+ " print(\"Assistant:\", value[\"messages\"][-1].content)"
+ ]
},
{
"cell_type": "markdown",
@@ -333,7 +408,11 @@
"id": "7451151f-41fc-4af0-9359-024ae51b7225",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U tavily-python\n%pip install -U langchain_community"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U tavily-python\n",
+ "%pip install -U langchain_community"
+ ]
},
{
"cell_type": "code",
@@ -341,7 +420,9 @@
"id": "0c52923c-5665-4f8c-a1ba-9799e369c49e",
"metadata": {},
"outputs": [],
- "source": ["_set_env(\"TAVILY_API_KEY\")"]
+ "source": [
+ "_set_env(\"TAVILY_API_KEY\")"
+ ]
},
{
"cell_type": "markdown",
@@ -371,7 +452,13 @@
"output_type": "execute_result"
}
],
- "source": ["from langchain_community.tools.tavily_search import TavilySearchResults\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\ntool.invoke(\"What's a 'node' in LangGraph?\")"]
+ "source": [
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "\n",
+ "tool = TavilySearchResults(max_results=2)\n",
+ "tools = [tool]\n",
+ "tool.invoke(\"What's a 'node' in LangGraph?\")"
+ ]
},
{
"cell_type": "markdown",
@@ -390,7 +477,34 @@
"id": "dc5af88b-47d2-43bf-9a2c-6c07506b1732",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n# Modification: tell the LLM which tools it can call\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START\n",
+ "from langgraph.graph.message import add_messages\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ "\n",
+ "\n",
+ "graph_builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "# Modification: tell the LLM which tools it can call\n",
+ "llm_with_tools = llm.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
+ "\n",
+ "\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)"
+ ]
},
{
"cell_type": "markdown",
@@ -410,7 +524,41 @@
"id": "12f1fc14-cd91-4cd4-9f2e-1d007f8beafc",
"metadata": {},
"outputs": [],
- "source": ["import json\n\nfrom langchain_core.messages import ToolMessage\n\n\nclass BasicToolNode:\n \"\"\"A node that runs the tools requested in the last AIMessage.\"\"\"\n\n def __init__(self, tools: list) -> None:\n self.tools_by_name = {tool.name: tool for tool in tools}\n\n def __call__(self, inputs: dict):\n if messages := inputs.get(\"messages\", []):\n message = messages[-1]\n else:\n raise ValueError(\"No message found in input\")\n outputs = []\n for tool_call in message.tool_calls:\n tool_result = self.tools_by_name[tool_call[\"name\"]].invoke(\n tool_call[\"args\"]\n )\n outputs.append(\n ToolMessage(\n content=json.dumps(tool_result),\n name=tool_call[\"name\"],\n tool_call_id=tool_call[\"id\"],\n )\n )\n return {\"messages\": outputs}\n\n\ntool_node = BasicToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)"]
+ "source": [
+ "import json\n",
+ "\n",
+ "from langchain_core.messages import ToolMessage\n",
+ "\n",
+ "\n",
+ "class BasicToolNode:\n",
+ " \"\"\"A node that runs the tools requested in the last AIMessage.\"\"\"\n",
+ "\n",
+ " def __init__(self, tools: list) -> None:\n",
+ " self.tools_by_name = {tool.name: tool for tool in tools}\n",
+ "\n",
+ " def __call__(self, inputs: dict):\n",
+ " if messages := inputs.get(\"messages\", []):\n",
+ " message = messages[-1]\n",
+ " else:\n",
+ " raise ValueError(\"No message found in input\")\n",
+ " outputs = []\n",
+ " for tool_call in message.tool_calls:\n",
+ " tool_result = self.tools_by_name[tool_call[\"name\"]].invoke(\n",
+ " tool_call[\"args\"]\n",
+ " )\n",
+ " outputs.append(\n",
+ " ToolMessage(\n",
+ " content=json.dumps(tool_result),\n",
+ " name=tool_call[\"name\"],\n",
+ " tool_call_id=tool_call[\"id\"],\n",
+ " )\n",
+ " )\n",
+ " return {\"messages\": outputs}\n",
+ "\n",
+ "\n",
+ "tool_node = BasicToolNode(tools=[tool])\n",
+ "graph_builder.add_node(\"tools\", tool_node)"
+ ]
},
{
"cell_type": "markdown",
@@ -434,7 +582,45 @@
"id": "d662df94-66ac-4c6c-92f0-4c93620f1c74",
"metadata": {},
"outputs": [],
- "source": ["from typing import Literal\n\n\ndef route_tools(\n state: State,\n) -> Literal[\"tools\", \"__end__\"]:\n \"\"\"\n Use in the conditional_edge to route to the ToolNode if the last message\n has tool calls. Otherwise, route to the end.\n \"\"\"\n if isinstance(state, list):\n ai_message = state[-1]\n elif messages := state.get(\"messages\", []):\n ai_message = messages[-1]\n else:\n raise ValueError(f\"No messages found in input state to tool_edge: {state}\")\n if hasattr(ai_message, \"tool_calls\") and len(ai_message.tool_calls) > 0:\n return \"tools\"\n return \"__end__\"\n\n\n# The `tools_condition` function returns \"tools\" if the chatbot asks to use a tool, and \"__end__\" if\n# it is fine directly responding. This conditional routing defines the main agent loop.\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n route_tools,\n # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node\n # It defaults to the identity function, but if you\n # want to use a node named something else apart from \"tools\",\n # You can update the value of the dictionary to something else\n # e.g., \"tools\": \"my_tools\"\n {\"tools\": \"tools\", \"__end__\": \"__end__\"},\n)\n# Any time a tool is called, we return to the chatbot to decide the next step\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\ngraph = graph_builder.compile()"]
+ "source": [
+ "from typing import Literal\n",
+ "\n",
+ "\n",
+ "def route_tools(\n",
+ " state: State,\n",
+ ") -> Literal[\"tools\", \"__end__\"]:\n",
+ " \"\"\"\n",
+ " Use in the conditional_edge to route to the ToolNode if the last message\n",
+ " has tool calls. Otherwise, route to the end.\n",
+ " \"\"\"\n",
+ " if isinstance(state, list):\n",
+ " ai_message = state[-1]\n",
+ " elif messages := state.get(\"messages\", []):\n",
+ " ai_message = messages[-1]\n",
+ " else:\n",
+ " raise ValueError(f\"No messages found in input state to tool_edge: {state}\")\n",
+ " if hasattr(ai_message, \"tool_calls\") and len(ai_message.tool_calls) > 0:\n",
+ " return \"tools\"\n",
+ " return \"__end__\"\n",
+ "\n",
+ "\n",
+ "# The `tools_condition` function returns \"tools\" if the chatbot asks to use a tool, and \"__end__\" if\n",
+ "# it is fine directly responding. This conditional routing defines the main agent loop.\n",
+ "graph_builder.add_conditional_edges(\n",
+ " \"chatbot\",\n",
+ " route_tools,\n",
+ " # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node\n",
+ " # It defaults to the identity function, but if you\n",
+ " # want to use a node named something else apart from \"tools\",\n",
+ " # You can update the value of the dictionary to something else\n",
+ " # e.g., \"tools\": \"my_tools\"\n",
+ " {\"tools\": \"tools\", \"__end__\": \"__end__\"},\n",
+ ")\n",
+ "# Any time a tool is called, we return to the chatbot to decide the next step\n",
+ "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
+ "graph_builder.add_edge(START, \"chatbot\")\n",
+ "graph = graph_builder.compile()"
+ ]
},
{
"cell_type": "markdown",
@@ -465,7 +651,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -550,7 +744,19 @@
]
}
],
- "source": ["from langchain_core.messages import BaseMessage\n\nwhile True:\n user_input = input(\"User: \")\n if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n print(\"Goodbye!\")\n break\n for event in graph.stream({\"messages\": [(\"user\", user_input)]}):\n for value in event.values():\n if isinstance(value[\"messages\"][-1], BaseMessage):\n print(\"Assistant:\", value[\"messages\"][-1].content)"]
+ "source": [
+ "from langchain_core.messages import BaseMessage\n",
+ "\n",
+ "while True:\n",
+ " user_input = input(\"User: \")\n",
+ " if user_input.lower() in [\"quit\", \"exit\", \"q\"]:\n",
+ " print(\"Goodbye!\")\n",
+ " break\n",
+ " for event in graph.stream({\"messages\": [(\"user\", user_input)]}):\n",
+ " for value in event.values():\n",
+ " if isinstance(value[\"messages\"][-1], BaseMessage):\n",
+ " print(\"Assistant:\", value[\"messages\"][-1].content)"
+ ]
},
{
"cell_type": "markdown",
@@ -639,7 +845,11 @@
"id": "6baafdf6-6803-4305-9381-9dc970468a4d",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")"]
+ "source": [
+ "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "\n",
+ "memory = SqliteSaver.from_conn_string(\":memory:\")"
+ ]
},
{
"cell_type": "markdown",
@@ -666,7 +876,49 @@
]
}
],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)\n\ntool_node = ToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n tools_condition,\n)\n# Any time a tool is called, we return to the chatbot to decide the next step\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.messages import BaseMessage\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START, END\n",
+ "from langgraph.graph.message import add_messages\n",
+ "from langgraph.prebuilt import ToolNode, tools_condition\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ "\n",
+ "\n",
+ "graph_builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "tool = TavilySearchResults(max_results=2)\n",
+ "tools = [tool]\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm_with_tools = llm.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
+ "\n",
+ "\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)\n",
+ "\n",
+ "tool_node = ToolNode(tools=[tool])\n",
+ "graph_builder.add_node(\"tools\", tool_node)\n",
+ "\n",
+ "graph_builder.add_conditional_edges(\n",
+ " \"chatbot\",\n",
+ " tools_condition,\n",
+ ")\n",
+ "# Any time a tool is called, we return to the chatbot to decide the next step\n",
+ "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
+ "graph_builder.add_edge(START, \"chatbot\")"
+ ]
},
{
"cell_type": "markdown",
@@ -682,7 +934,9 @@
"id": "a06548bf-81fa-4436-b4c1-f68601fb4187",
"metadata": {},
"outputs": [],
- "source": ["graph = graph_builder.compile(checkpointer=memory)"]
+ "source": [
+ "graph = graph_builder.compile(checkpointer=memory)"
+ ]
},
{
"cell_type": "markdown",
@@ -709,7 +963,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -725,7 +987,9 @@
"id": "be7b5abb-04ef-4d53-83d1-d4d3139cc43a",
"metadata": {},
"outputs": [],
- "source": ["config = {\"configurable\": {\"thread_id\": \"1\"}}"]
+ "source": [
+ "config = {\"configurable\": {\"thread_id\": \"1\"}}"
+ ]
},
{
"cell_type": "markdown",
@@ -754,7 +1018,16 @@
]
}
],
- "source": ["user_input = \"Hi there! My name is Will.\"\n\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "user_input = \"Hi there! My name is Will.\"\n",
+ "\n",
+ "# The config is the **second positional argument** to stream() or invoke()!\n",
+ "events = graph.stream(\n",
+ " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n",
+ ")\n",
+ "for event in events:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -785,7 +1058,16 @@
]
}
],
- "source": ["user_input = \"Remember my name?\"\n\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "user_input = \"Remember my name?\"\n",
+ "\n",
+ "# The config is the **second positional argument** to stream() or invoke()!\n",
+ "events = graph.stream(\n",
+ " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n",
+ ")\n",
+ "for event in events:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -816,7 +1098,16 @@
]
}
],
- "source": ["# The only difference is we change the `thread_id` here to \"2\" instead of \"1\"\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]},\n {\"configurable\": {\"thread_id\": \"2\"}},\n stream_mode=\"values\",\n)\nfor event in events:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "# The only difference is we change the `thread_id` here to \"2\" instead of \"1\"\n",
+ "events = graph.stream(\n",
+ " {\"messages\": [(\"user\", user_input)]},\n",
+ " {\"configurable\": {\"thread_id\": \"2\"}},\n",
+ " stream_mode=\"values\",\n",
+ ")\n",
+ "for event in events:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -845,7 +1136,10 @@
"output_type": "execute_result"
}
],
- "source": ["snapshot = graph.get_state(config)\nsnapshot"]
+ "source": [
+ "snapshot = graph.get_state(config)\n",
+ "snapshot"
+ ]
},
{
"cell_type": "code",
@@ -864,7 +1158,9 @@
"output_type": "execute_result"
}
],
- "source": ["snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)"]
+ "source": [
+ "snapshot.next # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)"
+ ]
},
{
"cell_type": "markdown",
@@ -960,7 +1256,51 @@
]
}
],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)\n\ntool_node = ToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n tools_condition,\n)\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.messages import BaseMessage\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.graph import StateGraph, 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",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ "\n",
+ "\n",
+ "graph_builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "tool = TavilySearchResults(max_results=2)\n",
+ "tools = [tool]\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm_with_tools = llm.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
+ "\n",
+ "\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)\n",
+ "\n",
+ "tool_node = ToolNode(tools=[tool])\n",
+ "graph_builder.add_node(\"tools\", tool_node)\n",
+ "\n",
+ "graph_builder.add_conditional_edges(\n",
+ " \"chatbot\",\n",
+ " tools_condition,\n",
+ ")\n",
+ "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
+ "graph_builder.add_edge(START, \"chatbot\")"
+ ]
},
{
"cell_type": "markdown",
@@ -976,7 +1316,15 @@
"id": "b0883e32-1a39-4ce9-ae32-bbd66708fd84",
"metadata": {},
"outputs": [],
- "source": ["graph = graph_builder.compile(\n checkpointer=memory,\n # This is new!\n interrupt_before=[\"tools\"],\n # Note: can also interrupt __after__ actions, if desired.\n # interrupt_after=[\"tools\"]\n)"]
+ "source": [
+ "graph = graph_builder.compile(\n",
+ " checkpointer=memory,\n",
+ " # This is new!\n",
+ " interrupt_before=[\"tools\"],\n",
+ " # Note: can also interrupt __after__ actions, if desired.\n",
+ " # interrupt_after=[\"tools\"]\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -1002,7 +1350,17 @@
]
}
],
- "source": ["user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n",
+ "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
+ "# The config is the **second positional argument** to stream() or invoke()!\n",
+ "events = graph.stream(\n",
+ " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n",
+ ")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1029,7 +1387,10 @@
"output_type": "execute_result"
}
],
- "source": ["snapshot = graph.get_state(config)\nsnapshot.next"]
+ "source": [
+ "snapshot = graph.get_state(config)\n",
+ "snapshot.next"
+ ]
},
{
"cell_type": "markdown",
@@ -1058,7 +1419,10 @@
"output_type": "execute_result"
}
],
- "source": ["existing_message = snapshot.values[\"messages\"][-1]\nexisting_message.tool_calls"]
+ "source": [
+ "existing_message = snapshot.values[\"messages\"][-1]\n",
+ "existing_message.tool_calls"
+ ]
},
{
"cell_type": "markdown",
@@ -1098,7 +1462,13 @@
]
}
],
- "source": ["# `None` will append nothing new to the current state, letting it resume as if it had never been interrupted\nevents = graph.stream(None, config, stream_mode=\"values\")\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "# `None` will append nothing new to the current state, letting it resume as if it had never been interrupted\n",
+ "events = graph.stream(None, config, stream_mode=\"values\")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1202,7 +1572,65 @@
]
}
],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\ngraph_builder = StateGraph(State)\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm_with_tools = llm.bind_tools(tools)\n\n\ndef chatbot(state: State):\n return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n\n\ngraph_builder.add_node(\"chatbot\", chatbot)\n\ntool_node = ToolNode(tools=[tool])\ngraph_builder.add_node(\"tools\", tool_node)\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n tools_condition,\n)\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = graph_builder.compile(\n checkpointer=memory,\n # This is new!\n interrupt_before=[\"tools\"],\n # Note: can also interrupt **after** actions, if desired.\n # interrupt_after=[\"tools\"]\n)\n\nuser_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream({\"messages\": [(\"user\", user_input)]}, config)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.messages import BaseMessage\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.graph import StateGraph, START\n",
+ "from langgraph.graph.message import add_messages\n",
+ "from langgraph.prebuilt import ToolNode, tools_condition\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ "\n",
+ "\n",
+ "graph_builder = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "tool = TavilySearchResults(max_results=2)\n",
+ "tools = [tool]\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "llm_with_tools = llm.bind_tools(tools)\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
+ "\n",
+ "\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)\n",
+ "\n",
+ "tool_node = ToolNode(tools=[tool])\n",
+ "graph_builder.add_node(\"tools\", tool_node)\n",
+ "\n",
+ "graph_builder.add_conditional_edges(\n",
+ " \"chatbot\",\n",
+ " tools_condition,\n",
+ ")\n",
+ "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
+ "graph_builder.add_edge(START, \"chatbot\")\n",
+ "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "graph = graph_builder.compile(\n",
+ " checkpointer=memory,\n",
+ " # This is new!\n",
+ " interrupt_before=[\"tools\"],\n",
+ " # Note: can also interrupt **after** actions, if desired.\n",
+ " # interrupt_after=[\"tools\"]\n",
+ ")\n",
+ "\n",
+ "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n",
+ "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
+ "# The config is the **second positional argument** to stream() or invoke()!\n",
+ "events = graph.stream({\"messages\": [(\"user\", user_input)]}, config)\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "code",
@@ -1225,7 +1653,11 @@
]
}
],
- "source": ["snapshot = graph.get_state(config)\nexisting_message = snapshot.values[\"messages\"][-1]\nexisting_message.pretty_print()"]
+ "source": [
+ "snapshot = graph.get_state(config)\n",
+ "existing_message = snapshot.values[\"messages\"][-1]\n",
+ "existing_message.pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1259,7 +1691,31 @@
]
}
],
- "source": ["from langchain_core.messages import AIMessage\n\nanswer = (\n \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n)\nnew_messages = [\n # The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.\n ToolMessage(content=answer, tool_call_id=existing_message.tool_calls[0][\"id\"]),\n # And then directly \"put words in the LLM's mouth\" by populating its response.\n AIMessage(content=answer),\n]\n\nnew_messages[-1].pretty_print()\ngraph.update_state(\n # Which state to update\n config,\n # The updated values to provide. The messages in our `State` are \"append-only\", meaning this will be appended\n # to the existing state. We will review how to update existing messages in the next section!\n {\"messages\": new_messages},\n)\n\nprint(\"\\n\\nLast 2 messages;\")\nprint(graph.get_state(config).values[\"messages\"][-2:])"]
+ "source": [
+ "from langchain_core.messages import AIMessage\n",
+ "\n",
+ "answer = (\n",
+ " \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n",
+ ")\n",
+ "new_messages = [\n",
+ " # The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.\n",
+ " ToolMessage(content=answer, tool_call_id=existing_message.tool_calls[0][\"id\"]),\n",
+ " # And then directly \"put words in the LLM's mouth\" by populating its response.\n",
+ " AIMessage(content=answer),\n",
+ "]\n",
+ "\n",
+ "new_messages[-1].pretty_print()\n",
+ "graph.update_state(\n",
+ " # Which state to update\n",
+ " config,\n",
+ " # The updated values to provide. The messages in our `State` are \"append-only\", meaning this will be appended\n",
+ " # to the existing state. We will review how to update existing messages in the next section!\n",
+ " {\"messages\": new_messages},\n",
+ ")\n",
+ "\n",
+ "print(\"\\n\\nLast 2 messages;\")\n",
+ "print(graph.get_state(config).values[\"messages\"][-2:])"
+ ]
},
{
"cell_type": "markdown",
@@ -1298,7 +1754,15 @@
"output_type": "execute_result"
}
],
- "source": ["graph.update_state(\n config,\n {\"messages\": [AIMessage(content=\"I'm an AI expert!\")]},\n # Which node for this function to act as. It will automatically continue\n # processing as if this node just ran.\n as_node=\"chatbot\",\n)"]
+ "source": [
+ "graph.update_state(\n",
+ " config,\n",
+ " {\"messages\": [AIMessage(content=\"I'm an AI expert!\")]},\n",
+ " # Which node for this function to act as. It will automatically continue\n",
+ " # processing as if this node just ran.\n",
+ " as_node=\"chatbot\",\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -1325,7 +1789,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -1350,7 +1822,11 @@
]
}
],
- "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][-3:])\nprint(snapshot.next)"]
+ "source": [
+ "snapshot = graph.get_state(config)\n",
+ "print(snapshot.values[\"messages\"][-3:])\n",
+ "print(snapshot.next)"
+ ]
},
{
"cell_type": "markdown",
@@ -1390,7 +1866,16 @@
]
}
],
- "source": ["user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"2\"}} # we'll use thread_id = 2 here\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n",
+ "config = {\"configurable\": {\"thread_id\": \"2\"}} # we'll use thread_id = 2 here\n",
+ "events = graph.stream(\n",
+ " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n",
+ ")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1434,7 +1919,31 @@
"output_type": "execute_result"
}
],
- "source": ["from langchain_core.messages import AIMessage\n\nsnapshot = graph.get_state(config)\nexisting_message = snapshot.values[\"messages\"][-1]\nprint(\"Original\")\nprint(\"Message ID\", existing_message.id)\nprint(existing_message.tool_calls[0])\nnew_tool_call = existing_message.tool_calls[0].copy()\nnew_tool_call[\"args\"][\"query\"] = \"LangGraph human-in-the-loop workflow\"\nnew_message = AIMessage(\n content=existing_message.content,\n tool_calls=[new_tool_call],\n # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages\n id=existing_message.id,\n)\n\nprint(\"Updated\")\nprint(new_message.tool_calls[0])\nprint(\"Message ID\", new_message.id)\ngraph.update_state(config, {\"messages\": [new_message]})\n\nprint(\"\\n\\nTool calls\")\ngraph.get_state(config).values[\"messages\"][-1].tool_calls"]
+ "source": [
+ "from langchain_core.messages import AIMessage\n",
+ "\n",
+ "snapshot = graph.get_state(config)\n",
+ "existing_message = snapshot.values[\"messages\"][-1]\n",
+ "print(\"Original\")\n",
+ "print(\"Message ID\", existing_message.id)\n",
+ "print(existing_message.tool_calls[0])\n",
+ "new_tool_call = existing_message.tool_calls[0].copy()\n",
+ "new_tool_call[\"args\"][\"query\"] = \"LangGraph human-in-the-loop workflow\"\n",
+ "new_message = AIMessage(\n",
+ " content=existing_message.content,\n",
+ " tool_calls=[new_tool_call],\n",
+ " # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages\n",
+ " id=existing_message.id,\n",
+ ")\n",
+ "\n",
+ "print(\"Updated\")\n",
+ "print(new_message.tool_calls[0])\n",
+ "print(\"Message ID\", new_message.id)\n",
+ "graph.update_state(config, {\"messages\": [new_message]})\n",
+ "\n",
+ "print(\"\\n\\nTool calls\")\n",
+ "graph.get_state(config).values[\"messages\"][-1].tool_calls"
+ ]
},
{
"cell_type": "markdown",
@@ -1474,7 +1983,12 @@
]
}
],
- "source": ["events = graph.stream(None, config, stream_mode=\"values\")\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "events = graph.stream(None, config, stream_mode=\"values\")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1509,7 +2023,21 @@
]
}
],
- "source": ["events = graph.stream(\n {\n \"messages\": (\n \"user\",\n \"Remember what I'm learning about?\",\n )\n },\n config,\n stream_mode=\"values\",\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "events = graph.stream(\n",
+ " {\n",
+ " \"messages\": (\n",
+ " \"user\",\n",
+ " \"Remember what I'm learning about?\",\n",
+ " )\n",
+ " },\n",
+ " config,\n",
+ " stream_mode=\"values\",\n",
+ ")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1543,7 +2071,25 @@
"id": "3cf7e042-1718-4625-ae30-a9917f595449",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import BaseMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n # This flag is new\n ask_human: bool"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.messages import BaseMessage\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.graph import StateGraph, START\n",
+ "from langgraph.graph.message import add_messages\n",
+ "from langgraph.prebuilt import ToolNode, tools_condition\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ " # This flag is new\n",
+ " ask_human: bool"
+ ]
},
{
"cell_type": "markdown",
@@ -1559,7 +2105,18 @@
"id": "e5192e54-6a28-42fe-a8a7-62d45d61f994",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.pydantic_v1 import BaseModel\n\n\nclass RequestAssistance(BaseModel):\n \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n\n To use this function, relay the user's 'request' so the expert can provide the right guidance.\n \"\"\"\n\n request: str"]
+ "source": [
+ "from langchain_core.pydantic_v1 import BaseModel\n",
+ "\n",
+ "\n",
+ "class RequestAssistance(BaseModel):\n",
+ " \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n",
+ "\n",
+ " To use this function, relay the user's 'request' so the expert can provide the right guidance.\n",
+ " \"\"\"\n",
+ "\n",
+ " request: str"
+ ]
},
{
"cell_type": "markdown",
@@ -1584,7 +2141,24 @@
]
}
],
- "source": ["tool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n# We can bind the llm to a tool definition, a pydantic model, or a json schema\nllm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n\n\ndef chatbot(state: State):\n response = llm_with_tools.invoke(state[\"messages\"])\n ask_human = False\n if (\n response.tool_calls\n and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n ):\n ask_human = True\n return {\"messages\": [response], \"ask_human\": ask_human}"]
+ "source": [
+ "tool = TavilySearchResults(max_results=2)\n",
+ "tools = [tool]\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "# We can bind the llm to a tool definition, a pydantic model, or a json schema\n",
+ "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " response = llm_with_tools.invoke(state[\"messages\"])\n",
+ " ask_human = False\n",
+ " if (\n",
+ " response.tool_calls\n",
+ " and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n",
+ " ):\n",
+ " ask_human = True\n",
+ " return {\"messages\": [response], \"ask_human\": ask_human}"
+ ]
},
{
"cell_type": "markdown",
@@ -1600,7 +2174,12 @@
"id": "3f4464d2-288b-4689-aaf0-329a55dcb85c",
"metadata": {},
"outputs": [],
- "source": ["graph_builder = StateGraph(State)\n\ngraph_builder.add_node(\"chatbot\", chatbot)\ngraph_builder.add_node(\"tools\", ToolNode(tools=[tool]))"]
+ "source": [
+ "graph_builder = StateGraph(State)\n",
+ "\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)\n",
+ "graph_builder.add_node(\"tools\", ToolNode(tools=[tool]))"
+ ]
},
{
"cell_type": "markdown",
@@ -1616,7 +2195,36 @@
"id": "1d70b5a4-ce50-47dc-aa43-ffb5c48c46fc",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import AIMessage, ToolMessage\n\n\ndef create_response(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response,\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef human_node(state: State):\n new_messages = []\n if not isinstance(state[\"messages\"][-1], ToolMessage):\n # Typically, the user will have updated the state during the interrupt.\n # If they choose not to, we will include a placeholder ToolMessage to\n # let the LLM continue.\n new_messages.append(\n create_response(\"No response from human.\", state[\"messages\"][-1])\n )\n return {\n # Append the new messages\n \"messages\": new_messages,\n # Unset the flag\n \"ask_human\": False,\n }\n\n\ngraph_builder.add_node(\"human\", human_node)"]
+ "source": [
+ "from langchain_core.messages import AIMessage, ToolMessage\n",
+ "\n",
+ "\n",
+ "def create_response(response: str, ai_message: AIMessage):\n",
+ " return ToolMessage(\n",
+ " content=response,\n",
+ " tool_call_id=ai_message.tool_calls[0][\"id\"],\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def human_node(state: State):\n",
+ " new_messages = []\n",
+ " if not isinstance(state[\"messages\"][-1], ToolMessage):\n",
+ " # Typically, the user will have updated the state during the interrupt.\n",
+ " # If they choose not to, we will include a placeholder ToolMessage to\n",
+ " # let the LLM continue.\n",
+ " new_messages.append(\n",
+ " create_response(\"No response from human.\", state[\"messages\"][-1])\n",
+ " )\n",
+ " return {\n",
+ " # Append the new messages\n",
+ " \"messages\": new_messages,\n",
+ " # Unset the flag\n",
+ " \"ask_human\": False,\n",
+ " }\n",
+ "\n",
+ "\n",
+ "graph_builder.add_node(\"human\", human_node)"
+ ]
},
{
"cell_type": "markdown",
@@ -1634,7 +2242,20 @@
"id": "586a0d07-8303-47f4-b3cf-3bdd043e762b",
"metadata": {},
"outputs": [],
- "source": ["def select_next_node(state: State):\n if state[\"ask_human\"]:\n return \"human\"\n # Otherwise, we can route as before\n return tools_condition(state)\n\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n select_next_node,\n {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n)"]
+ "source": [
+ "def select_next_node(state: State):\n",
+ " if state[\"ask_human\"]:\n",
+ " return \"human\"\n",
+ " # Otherwise, we can route as before\n",
+ " return tools_condition(state)\n",
+ "\n",
+ "\n",
+ "graph_builder.add_conditional_edges(\n",
+ " \"chatbot\",\n",
+ " select_next_node,\n",
+ " {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -1650,7 +2271,18 @@
"id": "84101737-0048-4635-9f68-45b0c508b6b6",
"metadata": {},
"outputs": [],
- "source": ["# The rest is the same\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(\"human\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = graph_builder.compile(\n checkpointer=memory,\n # We interrupt before 'human' here instead.\n interrupt_before=[\"human\"],\n)"]
+ "source": [
+ "# The rest is the same\n",
+ "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",
+ "graph = graph_builder.compile(\n",
+ " checkpointer=memory,\n",
+ " # We interrupt before 'human' here instead.\n",
+ " interrupt_before=[\"human\"],\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -1677,7 +2309,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -1713,7 +2353,17 @@
]
}
],
- "source": ["user_input = \"I need some expert guidance for building this AI agent. Could you request assistance for me?\"\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n# The config is the **second positional argument** to stream() or invoke()!\nevents = graph.stream(\n {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "user_input = \"I need some expert guidance for building this AI agent. Could you request assistance for me?\"\n",
+ "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
+ "# The config is the **second positional argument** to stream() or invoke()!\n",
+ "events = graph.stream(\n",
+ " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n",
+ ")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1740,7 +2390,10 @@
"output_type": "execute_result"
}
],
- "source": ["snapshot = graph.get_state(config)\nsnapshot.next"]
+ "source": [
+ "snapshot = graph.get_state(config)\n",
+ "snapshot.next"
+ ]
},
{
"cell_type": "markdown",
@@ -1772,7 +2425,15 @@
"output_type": "execute_result"
}
],
- "source": ["ai_message = snapshot.values[\"messages\"][-1]\nhuman_response = (\n \"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.\"\n \" It's much more reliable and extensible than simple autonomous agents.\"\n)\ntool_message = create_response(human_response, ai_message)\ngraph.update_state(config, {\"messages\": [tool_message]})"]
+ "source": [
+ "ai_message = snapshot.values[\"messages\"][-1]\n",
+ "human_response = (\n",
+ " \"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.\"\n",
+ " \" It's much more reliable and extensible than simple autonomous agents.\"\n",
+ ")\n",
+ "tool_message = create_response(human_response, ai_message)\n",
+ "graph.update_state(config, {\"messages\": [tool_message]})"
+ ]
},
{
"cell_type": "markdown",
@@ -1801,7 +2462,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.get_state(config).values[\"messages\"]"]
+ "source": [
+ "graph.get_state(config).values[\"messages\"]"
+ ]
},
{
"cell_type": "markdown",
@@ -1830,7 +2493,12 @@
]
}
],
- "source": ["events = graph.stream(None, config, stream_mode=\"values\")\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "events = graph.stream(None, config, stream_mode=\"values\")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -1979,7 +2647,108 @@
"id": "bb8a02de-a21b-4ef6-a714-7d6e44435e3a",
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated, Literal\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.messages import AIMessage, BaseMessage, ToolMessage\nfrom langchain_core.pydantic_v1 import BaseModel\nfrom typing_extensions import TypedDict\n\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\nfrom langgraph.prebuilt import ToolNode, tools_condition\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n # This flag is new\n ask_human: bool\n\n\nclass RequestAssistance(BaseModel):\n \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n\n To use this function, relay the user's 'request' so the expert can provide the right guidance.\n \"\"\"\n\n request: str\n\n\ntool = TavilySearchResults(max_results=2)\ntools = [tool]\nllm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n# We can bind the llm to a tool definition, a pydantic model, or a json schema\nllm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n\n\ndef chatbot(state: State):\n response = llm_with_tools.invoke(state[\"messages\"])\n ask_human = False\n if (\n response.tool_calls\n and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n ):\n ask_human = True\n return {\"messages\": [response], \"ask_human\": ask_human}\n\n\ngraph_builder = StateGraph(State)\n\ngraph_builder.add_node(\"chatbot\", chatbot)\ngraph_builder.add_node(\"tools\", ToolNode(tools=[tool]))\n\n\ndef create_response(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response,\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef human_node(state: State):\n new_messages = []\n if not isinstance(state[\"messages\"][-1], ToolMessage):\n # Typically, the user will have updated the state during the interrupt.\n # If they choose not to, we will include a placeholder ToolMessage to\n # let the LLM continue.\n new_messages.append(\n create_response(\"No response from human.\", state[\"messages\"][-1])\n )\n return {\n # Append the new messages\n \"messages\": new_messages,\n # Unset the flag\n \"ask_human\": False,\n }\n\n\ngraph_builder.add_node(\"human\", human_node)\n\n\ndef select_next_node(state: State) -> Literal[\"human\", \"tools\", \"__end__\"]:\n if state[\"ask_human\"]:\n return \"human\"\n # Otherwise, we can route as before\n return tools_condition(state)\n\n\ngraph_builder.add_conditional_edges(\n \"chatbot\",\n select_next_node,\n {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n)\ngraph_builder.add_edge(\"tools\", \"chatbot\")\ngraph_builder.add_edge(\"human\", \"chatbot\")\ngraph_builder.add_edge(START, \"chatbot\")\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = graph_builder.compile(\n checkpointer=memory,\n interrupt_before=[\"human\"],\n)"]
+ "source": [
+ "from typing import Annotated, Literal\n",
+ "\n",
+ "from langchain_anthropic import ChatAnthropic\n",
+ "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+ "from langchain_core.messages import AIMessage, BaseMessage, ToolMessage\n",
+ "from langchain_core.pydantic_v1 import BaseModel\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.checkpoint.sqlite import SqliteSaver\n",
+ "from langgraph.graph import StateGraph, START\n",
+ "from langgraph.graph.message import add_messages\n",
+ "from langgraph.prebuilt import ToolNode, tools_condition\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ " # This flag is new\n",
+ " ask_human: bool\n",
+ "\n",
+ "\n",
+ "class RequestAssistance(BaseModel):\n",
+ " \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n",
+ "\n",
+ " To use this function, relay the user's 'request' so the expert can provide the right guidance.\n",
+ " \"\"\"\n",
+ "\n",
+ " request: str\n",
+ "\n",
+ "\n",
+ "tool = TavilySearchResults(max_results=2)\n",
+ "tools = [tool]\n",
+ "llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
+ "# We can bind the llm to a tool definition, a pydantic model, or a json schema\n",
+ "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n",
+ "\n",
+ "\n",
+ "def chatbot(state: State):\n",
+ " response = llm_with_tools.invoke(state[\"messages\"])\n",
+ " ask_human = False\n",
+ " if (\n",
+ " response.tool_calls\n",
+ " and response.tool_calls[0][\"name\"] == RequestAssistance.__name__\n",
+ " ):\n",
+ " ask_human = True\n",
+ " return {\"messages\": [response], \"ask_human\": ask_human}\n",
+ "\n",
+ "\n",
+ "graph_builder = StateGraph(State)\n",
+ "\n",
+ "graph_builder.add_node(\"chatbot\", chatbot)\n",
+ "graph_builder.add_node(\"tools\", ToolNode(tools=[tool]))\n",
+ "\n",
+ "\n",
+ "def create_response(response: str, ai_message: AIMessage):\n",
+ " return ToolMessage(\n",
+ " content=response,\n",
+ " tool_call_id=ai_message.tool_calls[0][\"id\"],\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def human_node(state: State):\n",
+ " new_messages = []\n",
+ " if not isinstance(state[\"messages\"][-1], ToolMessage):\n",
+ " # Typically, the user will have updated the state during the interrupt.\n",
+ " # If they choose not to, we will include a placeholder ToolMessage to\n",
+ " # let the LLM continue.\n",
+ " new_messages.append(\n",
+ " create_response(\"No response from human.\", state[\"messages\"][-1])\n",
+ " )\n",
+ " return {\n",
+ " # Append the new messages\n",
+ " \"messages\": new_messages,\n",
+ " # Unset the flag\n",
+ " \"ask_human\": False,\n",
+ " }\n",
+ "\n",
+ "\n",
+ "graph_builder.add_node(\"human\", human_node)\n",
+ "\n",
+ "\n",
+ "def select_next_node(state: State) -> Literal[\"human\", \"tools\", \"__end__\"]:\n",
+ " if state[\"ask_human\"]:\n",
+ " return \"human\"\n",
+ " # Otherwise, we can route as before\n",
+ " return tools_condition(state)\n",
+ "\n",
+ "\n",
+ "graph_builder.add_conditional_edges(\n",
+ " \"chatbot\",\n",
+ " select_next_node,\n",
+ " {\"human\": \"human\", \"tools\": \"tools\", \"__end__\": \"__end__\"},\n",
+ ")\n",
+ "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
+ "graph_builder.add_edge(\"human\", \"chatbot\")\n",
+ "graph_builder.add_edge(START, \"chatbot\")\n",
+ "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
+ "graph = graph_builder.compile(\n",
+ " checkpointer=memory,\n",
+ " interrupt_before=[\"human\"],\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -1998,7 +2767,15 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "try:\n",
+ " display(Image(graph.get_graph().draw_mermaid_png()))\n",
+ "except Exception:\n",
+ " # This requires some extra dependencies and is optional\n",
+ " pass"
+ ]
},
{
"cell_type": "markdown",
@@ -2051,7 +2828,21 @@
]
}
],
- "source": ["config = {\"configurable\": {\"thread_id\": \"1\"}}\nevents = graph.stream(\n {\n \"messages\": [\n (\"user\", \"I'm learning LangGraph. Could you do some research on it for me?\")\n ]\n },\n config,\n stream_mode=\"values\",\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
+ "events = graph.stream(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " (\"user\", \"I'm learning LangGraph. Could you do some research on it for me?\")\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " stream_mode=\"values\",\n",
+ ")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "code",
@@ -2096,7 +2887,20 @@
]
}
],
- "source": ["events = graph.stream(\n {\n \"messages\": [\n (\"user\", \"Ya that's helpful. Maybe I'll build an autonomous agent with it!\")\n ]\n },\n config,\n stream_mode=\"values\",\n)\nfor event in events:\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "events = graph.stream(\n",
+ " {\n",
+ " \"messages\": [\n",
+ " (\"user\", \"Ya that's helpful. Maybe I'll build an autonomous agent with it!\")\n",
+ " ]\n",
+ " },\n",
+ " config,\n",
+ " stream_mode=\"values\",\n",
+ ")\n",
+ "for event in events:\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
@@ -2135,7 +2939,15 @@
]
}
],
- "source": ["to_replay = None\nfor state in graph.get_state_history(config):\n print(\"Num Messages: \", len(state.values[\"messages\"]), \"Next: \", state.next)\n print(\"-\" * 80)\n if len(state.values[\"messages\"]) == 6:\n # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.\n to_replay = state"]
+ "source": [
+ "to_replay = None\n",
+ "for state in graph.get_state_history(config):\n",
+ " print(\"Num Messages: \", len(state.values[\"messages\"]), \"Next: \", state.next)\n",
+ " print(\"-\" * 80)\n",
+ " if len(state.values[\"messages\"]) == 6:\n",
+ " # We are somewhat arbitrarily selecting a specific state based on the number of chat messages in the state.\n",
+ " to_replay = state"
+ ]
},
{
"cell_type": "markdown",
@@ -2162,7 +2974,10 @@
]
}
],
- "source": ["print(to_replay.next)\nprint(to_replay.config)"]
+ "source": [
+ "print(to_replay.next)\n",
+ "print(to_replay.config)"
+ ]
},
{
"cell_type": "markdown",
@@ -2208,7 +3023,12 @@
]
}
],
- "source": ["# The `thread_ts` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.\nfor event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n if \"messages\" in event:\n event[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "# The `thread_ts` in the `to_replay.config` corresponds to a state we've persisted to our checkpointer.\n",
+ "for event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n",
+ " if \"messages\" in event:\n",
+ " event[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "markdown",
diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb
index f5d113154..22440bdc4 100644
--- a/examples/persistence_postgres.ipynb
+++ b/examples/persistence_postgres.ipynb
@@ -42,7 +42,16 @@
"source": [
"\"\"\"Implementation of a langgraph checkpoint saver using Postgres.\"\"\"\n",
"from contextlib import asynccontextmanager, contextmanager\n",
- "from typing import Any, AsyncGenerator, AsyncIterator, Generator, Optional, Union, Tuple, List\n",
+ "from typing import (\n",
+ " Any,\n",
+ " AsyncGenerator,\n",
+ " AsyncIterator,\n",
+ " Generator,\n",
+ " Optional,\n",
+ " Union,\n",
+ " Tuple,\n",
+ " List,\n",
+ ")\n",
"\n",
"import psycopg\n",
"from langchain_core.runnables import RunnableConfig\n",
@@ -136,8 +145,7 @@
" sync_connection: Optional[Union[psycopg.Connection, ConnectionPool]] = None,\n",
" async_connection: Optional[\n",
" Union[psycopg.AsyncConnection, AsyncConnectionPool]\n",
- " ] = None\n",
- " \n",
+ " ] = None,\n",
" ):\n",
" super().__init__(serde=JsonPlusSerializer())\n",
" self.sync_connection = sync_connection\n",
@@ -168,15 +176,12 @@
" );\n",
" \"\"\"\n",
"\n",
- " \n",
- "\n",
" @staticmethod\n",
" def create_tables(connection: Union[psycopg.Connection, ConnectionPool], /) -> None:\n",
" \"\"\"Create the schema for the checkpoint saver.\"\"\"\n",
" with _get_sync_connection(connection) as conn:\n",
" with conn.cursor() as cur:\n",
" cur.execute(PostgresSaver.CREATE_TABLES_QUERY)\n",
- " \n",
"\n",
" @staticmethod\n",
" async def acreate_tables(\n",
@@ -209,7 +214,12 @@
" metadata = EXCLUDED.metadata;\n",
" \"\"\"\n",
"\n",
- " def put(self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata) -> RunnableConfig:\n",
+ " def put(\n",
+ " self,\n",
+ " config: RunnableConfig,\n",
+ " checkpoint: Checkpoint,\n",
+ " metadata: CheckpointMetadata,\n",
+ " ) -> RunnableConfig:\n",
" \"\"\"Put the checkpoint for the given configuration.\n",
" Args:\n",
" config: The configuration for the checkpoint.\n",
@@ -244,7 +254,10 @@
" }\n",
"\n",
" async def aput(\n",
- " self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata\n",
+ " self,\n",
+ " config: RunnableConfig,\n",
+ " checkpoint: Checkpoint,\n",
+ " metadata: CheckpointMetadata,\n",
" ) -> RunnableConfig:\n",
" \"\"\"Put the checkpoint for the given configuration.\n",
" Args:\n",
@@ -403,18 +416,18 @@
" if value:\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" return CheckpointTuple(\n",
- " config=config,\n",
- " checkpoint=self.serde.loads(checkpoint),\n",
- " metadata=self.serde.loads(metadata),\n",
- " parent_config={\n",
- " \"configurable\": {\n",
- " \"thread_id\": thread_id,\n",
- " \"thread_ts\": thread_ts,\n",
- " }\n",
+ " config=config,\n",
+ " checkpoint=self.serde.loads(checkpoint),\n",
+ " metadata=self.serde.loads(metadata),\n",
+ " parent_config={\n",
+ " \"configurable\": {\n",
+ " \"thread_id\": thread_id,\n",
+ " \"thread_ts\": thread_ts,\n",
" }\n",
- " if thread_ts\n",
- " else None,\n",
- " )\n",
+ " }\n",
+ " if thread_ts\n",
+ " else None,\n",
+ " )\n",
" else:\n",
" cur.execute(\n",
" self.GET_CHECKPOINT_QUERY,\n",
@@ -473,18 +486,18 @@
" if value:\n",
" checkpoint, metadata, thread_ts, parent_ts = value\n",
" return CheckpointTuple(\n",
- " config=config,\n",
- " checkpoint=self.serde.loads(checkpoint),\n",
- " metadata=self.serde.loads(metadata),\n",
- " parent_config={\n",
- " \"configurable\": {\n",
- " \"thread_id\": thread_id,\n",
- " \"thread_ts\": thread_ts,\n",
- " }\n",
+ " config=config,\n",
+ " checkpoint=self.serde.loads(checkpoint),\n",
+ " metadata=self.serde.loads(metadata),\n",
+ " parent_config={\n",
+ " \"configurable\": {\n",
+ " \"thread_id\": thread_id,\n",
+ " \"thread_ts\": thread_ts,\n",
" }\n",
- " if thread_ts\n",
- " else None,\n",
- " )\n",
+ " }\n",
+ " if thread_ts\n",
+ " else None,\n",
+ " )\n",
" else:\n",
" await cur.execute(\n",
" self.GET_CHECKPOINT_QUERY,\n",
@@ -663,9 +676,7 @@
" max_size=20,\n",
")\n",
"\n",
- "checkpointer = PostgresSaver(\n",
- " sync_connection=pool\n",
- ")\n",
+ "checkpointer = PostgresSaver(sync_connection=pool)\n",
"checkpointer.create_tables(pool)"
]
},
@@ -761,9 +772,7 @@
"from psycopg import Connection\n",
"\n",
"with Connection.connect(DB_URI) as conn:\n",
- " checkpointer = PostgresSaver(\n",
- " sync_connection=conn\n",
- " )\n",
+ " checkpointer = PostgresSaver(sync_connection=conn)\n",
"\n",
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
" config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
@@ -833,9 +842,7 @@
" max_size=20,\n",
")\n",
"\n",
- "checkpointer = PostgresSaver(\n",
- " async_connection=pool\n",
- ")\n",
+ "checkpointer = PostgresSaver(async_connection=pool)\n",
"await checkpointer.acreate_tables(pool)"
]
},
@@ -848,7 +855,9 @@
"source": [
"graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
"config = {\"configurable\": {\"thread_id\": \"3\"}}\n",
- "res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)"
+ "res = await graph.ainvoke(\n",
+ " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n",
+ ")"
]
},
{
@@ -900,12 +909,12 @@
"from psycopg import AsyncConnection\n",
"\n",
"async with await AsyncConnection.connect(DB_URI) as conn:\n",
- " checkpointer = PostgresSaver(\n",
- " async_connection=conn\n",
- " )\n",
+ " checkpointer = PostgresSaver(async_connection=conn)\n",
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
" config = {\"configurable\": {\"thread_id\": \"4\"}}\n",
- " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n",
+ " res = await graph.ainvoke(\n",
+ " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n",
+ " )\n",
" checkpoint_tuples = [c async for c in checkpointer.alist(config)]"
]
},
diff --git a/examples/rag/langgraph_adaptive_rag.ipynb b/examples/rag/langgraph_adaptive_rag.ipynb
index a7cbbff0b..8caa57bfa 100644
--- a/examples/rag/langgraph_adaptive_rag.ipynb
+++ b/examples/rag/langgraph_adaptive_rag.ipynb
@@ -46,7 +46,9 @@
"id": "53d1a740-9fea-4a6e-8f95-fb9dbf1c80a1",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python"]
+ "source": [
+ "%%capture --no-stderr\n! pip install -U langchain_community tiktoken langchain-openai langchain-cohere langchainhub chromadb langchain langgraph tavily-python"
+ ]
},
{
"cell_type": "code",
@@ -54,7 +56,9 @@
"id": "222f204d-956f-4128-b597-2c698120edda",
"metadata": {},
"outputs": [],
- "source": ["### LLMs\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\"\nos.environ[\"COHERE_API_KEY\"] = \"\"\nos.environ[\"TAVILY_API_KEY\"] = \"\""]
+ "source": [
+ "### LLMs\nimport os\n\nos.environ[\"OPENAI_API_KEY\"] = \"\"\nos.environ[\"COHERE_API_KEY\"] = \"\"\nos.environ[\"TAVILY_API_KEY\"] = \"\""
+ ]
},
{
"cell_type": "markdown",
@@ -72,7 +76,9 @@
"id": "08edba00-988a-478b-96fc-ae0199cbef49",
"metadata": {},
"outputs": [],
- "source": ["### Tracing (optional)\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""]
+ "source": [
+ "### Tracing (optional)\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""
+ ]
},
{
"cell_type": "markdown",
@@ -88,7 +94,9 @@
"id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c",
"metadata": {},
"outputs": [],
- "source": ["### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\n### from langchain_cohere import CohereEmbeddings\n\n# Set embeddings\nembd = OpenAIEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=500, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=embd,\n)\nretriever = vectorstore.as_retriever()"]
+ "source": [
+ "### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\n\n### from langchain_cohere import CohereEmbeddings\n\n# Set embeddings\nembd = OpenAIEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=500, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=embd,\n)\nretriever = vectorstore.as_retriever()"
+ ]
},
{
"cell_type": "markdown",
@@ -113,7 +121,9 @@
]
}
],
- "source": ["### Router\n\nfrom typing import Literal\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass RouteQuery(BaseModel):\n \"\"\"Route a user query to the most relevant datasource.\"\"\"\n\n datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n ...,\n description=\"Given a user question choose to route it to web search or a vectorstore.\",\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_router = llm.with_structured_output(RouteQuery)\n\n# Prompt\nsystem = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nprint(\n question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n )\n)\nprint(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"]
+ "source": [
+ "### Router\n\nfrom typing import Literal\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\n\n# Data model\nclass RouteQuery(BaseModel):\n \"\"\"Route a user query to the most relevant datasource.\"\"\"\n\n datasource: Literal[\"vectorstore\", \"web_search\"] = Field(\n ...,\n description=\"Given a user question choose to route it to web search or a vectorstore.\",\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_router = llm.with_structured_output(RouteQuery)\n\n# Prompt\nsystem = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nprint(\n question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n )\n)\nprint(question_router.invoke({\"question\": \"What are the types of agent memory?\"}))"
+ ]
},
{
"cell_type": "code",
@@ -129,7 +139,9 @@
]
}
],
- "source": ["### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
+ "source": [
+ "### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
},
{
"cell_type": "code",
@@ -145,7 +157,9 @@
]
}
],
- "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
+ "source": [
+ "### Generate\n\nfrom langchain import hub\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"
+ ]
},
{
"cell_type": "code",
@@ -164,7 +178,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
+ "source": [
+ "### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeHallucinations)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n \n Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -183,7 +199,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
+ "source": [
+ "### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# LLM with function call\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer)\n\n# Prompt\nsystem = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n \n Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -202,7 +220,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"]
+ "source": [
+ "### Question Re-writer\n\n# LLM\nllm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)\n\n# Prompt\nsystem = \"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.\"\"\"\nre_write_prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system),\n (\n \"human\",\n \"Here is the initial question: \\n\\n {question} \\n Formulate an improved question.\",\n ),\n ]\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"
+ ]
},
{
"cell_type": "markdown",
@@ -218,7 +238,9 @@
"id": "01d829bb-1074-4976-b650-ead41dcb9788",
"metadata": {},
"outputs": [],
- "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"]
+ "source": [
+ "### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"
+ ]
},
{
"cell_type": "markdown",
@@ -238,7 +260,9 @@
"id": "e723fcdb-06e6-402d-912e-899795b78408",
"metadata": {},
"outputs": [],
- "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
+ "source": [
+ "from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"
+ ]
},
{
"cell_type": "markdown",
@@ -254,7 +278,9 @@
"id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a",
"metadata": {},
"outputs": [],
- "source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n if source.datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source.datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
+ "source": [
+ "from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n if source.datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source.datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""
+ ]
},
{
"cell_type": "markdown",
@@ -270,7 +296,51 @@
"id": "67854e07-9293-4c3c-bf9a-bc9a605570ee",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"web_search\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"web_search\": \"web_search\",\n \"vectorstore\": \"retrieve\",\n })\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generatae\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
},
{
"cell_type": "code",
@@ -302,7 +372,9 @@
]
}
],
- "source": ["from pprint import pprint\n\n# Run\ninputs = {\n \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
+ "source": [
+ "from pprint import pprint\n\n# Run\ninputs = {\n \"question\": \"What player at the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -353,7 +425,9 @@
]
}
],
- "source": ["# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
+ "source": [
+ "# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -371,7 +445,9 @@
"id": "19ac1f6f-2d84-488f-8a0e-7ee2a46b0f71",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/rag/langgraph_adaptive_rag_cohere.ipynb b/examples/rag/langgraph_adaptive_rag_cohere.ipynb
index 93447acab..dbc957f91 100644
--- a/examples/rag/langgraph_adaptive_rag_cohere.ipynb
+++ b/examples/rag/langgraph_adaptive_rag_cohere.ipynb
@@ -53,7 +53,9 @@
"id": "f6c329ba-cb85-4576-9828-4f2ac648d1a6",
"metadata": {},
"outputs": [],
- "source": ["! pip install --quiet langchain langchain_cohere langchain-openai tiktoken langchainhub chromadb langgraph"]
+ "source": [
+ "! pip install --quiet langchain langchain_cohere langchain-openai tiktoken langchainhub chromadb langgraph"
+ ]
},
{
"cell_type": "code",
@@ -63,7 +65,9 @@
"id": "222f204d-956f-4128-b597-2c698120edda"
},
"outputs": [],
- "source": ["### LLMs\nimport os\n\nos.environ[\"COHERE_API_KEY\"] = \"\""]
+ "source": [
+ "### LLMs\nimport os\n\nos.environ[\"COHERE_API_KEY\"] = \"\""
+ ]
},
{
"cell_type": "code",
@@ -73,7 +77,9 @@
"id": "08edba00-988a-478b-96fc-ae0199cbef49"
},
"outputs": [],
- "source": ["# ### Tracing (optional)\n# os.environ['LANGCHAIN_TRACING_V2'] = 'true'\n# os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'\n# os.environ['LANGCHAIN_API_KEY'] =''"]
+ "source": [
+ "# ### Tracing (optional)\n# os.environ['LANGCHAIN_TRACING_V2'] = 'true'\n# os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'\n# os.environ['LANGCHAIN_API_KEY'] =''"
+ ]
},
{
"cell_type": "markdown",
@@ -93,7 +99,9 @@
"id": "b224e5ba-50ca-495a-a7fa-0f75a080e03c"
},
"outputs": [],
- "source": ["### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_cohere import CohereEmbeddings\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\n\n# Set embeddings\nembd = CohereEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=512, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n embedding=embd,\n)\n\nretriever = vectorstore.as_retriever()"]
+ "source": [
+ "### Build Index\n\nfrom langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_cohere import CohereEmbeddings\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\n\n# Set embeddings\nembd = CohereEmbeddings()\n\n# Docs to index\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Split\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=512, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorstore\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n embedding=embd,\n)\n\nretriever = vectorstore.as_retriever()"
+ ]
},
{
"cell_type": "markdown",
@@ -139,7 +147,9 @@
]
}
],
- "source": ["### Router\n\nfrom langchain_cohere import ChatCohere\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\n# Data model\nclass web_search(BaseModel):\n \"\"\"\n The internet. Use web_search for questions that are related to anything else than agents, prompt engineering, and adversarial attacks.\n \"\"\"\n\n query: str = Field(description=\"The query to use when searching the internet.\")\n\n\nclass vectorstore(BaseModel):\n \"\"\"\n A vectorstore containing documents related to agents, prompt engineering, and adversarial attacks. Use the vectorstore for questions on these topics.\n \"\"\"\n\n query: str = Field(description=\"The query to use when searching the vectorstore.\")\n\n\n# Preamble\npreamble = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n\n# LLM with tool use and preamble\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_router = llm.bind_tools(\n tools=[web_search, vectorstore], preamble=preamble\n)\n\n# Prompt\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nresponse = question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n)\nprint(response.response_metadata[\"tool_calls\"])\nresponse = question_router.invoke({\"question\": \"What are the types of agent memory?\"})\nprint(response.response_metadata[\"tool_calls\"])\nresponse = question_router.invoke({\"question\": \"Hi how are you?\"})\nprint(\"tool_calls\" in response.response_metadata)"]
+ "source": [
+ "### Router\n\nfrom langchain_cohere import ChatCohere\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\n# Data model\nclass web_search(BaseModel):\n \"\"\"\n The internet. Use web_search for questions that are related to anything else than agents, prompt engineering, and adversarial attacks.\n \"\"\"\n\n query: str = Field(description=\"The query to use when searching the internet.\")\n\n\nclass vectorstore(BaseModel):\n \"\"\"\n A vectorstore containing documents related to agents, prompt engineering, and adversarial attacks. Use the vectorstore for questions on these topics.\n \"\"\"\n\n query: str = Field(description=\"The query to use when searching the vectorstore.\")\n\n\n# Preamble\npreamble = \"\"\"You are an expert at routing a user question to a vectorstore or web search.\nThe vectorstore contains documents related to agents, prompt engineering, and adversarial attacks.\nUse the vectorstore for questions on these topics. Otherwise, use web-search.\"\"\"\n\n# LLM with tool use and preamble\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_router = llm.bind_tools(\n tools=[web_search, vectorstore], preamble=preamble\n)\n\n# Prompt\nroute_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"{question}\"),\n ]\n)\n\nquestion_router = route_prompt | structured_llm_router\nresponse = question_router.invoke(\n {\"question\": \"Who will the Bears draft first in the NFL draft?\"}\n)\nprint(response.response_metadata[\"tool_calls\"])\nresponse = question_router.invoke({\"question\": \"What are the types of agent memory?\"})\nprint(response.response_metadata[\"tool_calls\"])\nresponse = question_router.invoke({\"question\": \"Hi how are you?\"})\nprint(\"tool_calls\" in response.response_metadata)"
+ ]
},
{
"cell_type": "code",
@@ -161,7 +171,9 @@
]
}
],
- "source": ["### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# Prompt\npreamble = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n\nIf the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\nGive a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments, preamble=preamble)\n\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"types of agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nresponse = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\nprint(response)"]
+ "source": [
+ "### Retrieval Grader\n\n\n# Data model\nclass GradeDocuments(BaseModel):\n \"\"\"Binary score for relevance check on retrieved documents.\"\"\"\n\n binary_score: str = Field(\n description=\"Documents are relevant to the question, 'yes' or 'no'\"\n )\n\n\n# Prompt\npreamble = \"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n\nIf the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\nGive a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeDocuments, preamble=preamble)\n\ngrade_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"Retrieved document: \\n\\n {document} \\n\\n User question: {question}\"),\n ]\n)\n\nretrieval_grader = grade_prompt | structured_llm_grader\nquestion = \"types of agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nresponse = retrieval_grader.invoke({\"question\": question, \"document\": doc_txt})\nprint(response)"
+ ]
},
{
"cell_type": "markdown",
@@ -193,7 +205,9 @@
]
}
],
- "source": ["### Generate\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Preamble\npreamble = \"\"\"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\"\"\"\n\n# LLM\nllm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n\n\n# Prompt\ndef prompt(x):\n return ChatPromptTemplate.from_messages(\n [\n HumanMessage(\n f\"Question: {x['question']} \\nAnswer: \",\n additional_kwargs={\"documents\": x[\"documents\"]},\n )\n ]\n )\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"]
+ "source": [
+ "### Generate\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Preamble\npreamble = \"\"\"You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\"\"\"\n\n# LLM\nllm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n\n\n# Prompt\ndef prompt(x):\n return ChatPromptTemplate.from_messages(\n [\n HumanMessage(\n f\"Question: {x['question']} \\nAnswer: \",\n additional_kwargs={\"documents\": x[\"documents\"]},\n )\n ]\n )\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"
+ ]
},
{
"cell_type": "code",
@@ -209,7 +223,9 @@
]
}
],
- "source": ["### LLM fallback\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Preamble\npreamble = \"\"\"You are an assistant for question-answering tasks. Answer the question based upon your knowledge. Use three sentences maximum and keep the answer concise.\"\"\"\n\n# LLM\nllm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n\n\n# Prompt\ndef prompt(x):\n return ChatPromptTemplate.from_messages(\n [HumanMessage(f\"Question: {x['question']} \\nAnswer: \")]\n )\n\n\n# Chain\nllm_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"Hi how are you?\"\ngeneration = llm_chain.invoke({\"question\": question})\nprint(generation)"]
+ "source": [
+ "### LLM fallback\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Preamble\npreamble = \"\"\"You are an assistant for question-answering tasks. Answer the question based upon your knowledge. Use three sentences maximum and keep the answer concise.\"\"\"\n\n# LLM\nllm = ChatCohere(model_name=\"command-r\", temperature=0).bind(preamble=preamble)\n\n\n# Prompt\ndef prompt(x):\n return ChatPromptTemplate.from_messages(\n [HumanMessage(f\"Question: {x['question']} \\nAnswer: \")]\n )\n\n\n# Chain\nllm_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"Hi how are you?\"\ngeneration = llm_chain.invoke({\"question\": question})\nprint(generation)"
+ ]
},
{
"cell_type": "code",
@@ -234,7 +250,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# Preamble\npreamble = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n\nGive a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(\n GradeHallucinations, preamble=preamble\n)\n\n# Prompt\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n # (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
+ "source": [
+ "### Hallucination Grader\n\n\n# Data model\nclass GradeHallucinations(BaseModel):\n \"\"\"Binary score for hallucination present in generation answer.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer is grounded in the facts, 'yes' or 'no'\"\n )\n\n\n# Preamble\npreamble = \"\"\"You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \\n\nGive a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(\n GradeHallucinations, preamble=preamble\n)\n\n# Prompt\nhallucination_prompt = ChatPromptTemplate.from_messages(\n [\n # (\"system\", system),\n (\"human\", \"Set of facts: \\n\\n {documents} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nhallucination_grader = hallucination_prompt | structured_llm_grader\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -259,7 +277,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# Preamble\npreamble = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n\nGive a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer, preamble=preamble)\n\n# Prompt\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
+ "source": [
+ "### Answer Grader\n\n\n# Data model\nclass GradeAnswer(BaseModel):\n \"\"\"Binary score to assess answer addresses question.\"\"\"\n\n binary_score: str = Field(\n description=\"Answer addresses the question, 'yes' or 'no'\"\n )\n\n\n# Preamble\npreamble = \"\"\"You are a grader assessing whether an answer addresses / resolves a question \\n\nGive a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.\"\"\"\n\n# LLM with function call\nllm = ChatCohere(model=\"command-r\", temperature=0)\nstructured_llm_grader = llm.with_structured_output(GradeAnswer, preamble=preamble)\n\n# Prompt\nanswer_prompt = ChatPromptTemplate.from_messages(\n [\n (\"human\", \"User question: \\n\\n {question} \\n\\n LLM generation: {generation}\"),\n ]\n)\n\nanswer_grader = answer_prompt | structured_llm_grader\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
},
{
"cell_type": "markdown",
@@ -279,7 +299,9 @@
"id": "01d829bb-1074-4976-b650-ead41dcb9788"
},
"outputs": [],
- "source": ["### Search\n# os.environ['TAVILY_API_KEY'] =''\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults()"]
+ "source": [
+ "### Search\n# os.environ['TAVILY_API_KEY'] =''\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults()"
+ ]
},
{
"cell_type": "markdown",
@@ -303,7 +325,9 @@
"id": "e723fcdb-06e6-402d-912e-899795b78408"
},
"outputs": [],
- "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"|\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
+ "source": [
+ "from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"|\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"
+ ]
},
{
"cell_type": "markdown",
@@ -323,7 +347,9 @@
"id": "b76b5ec3-0720-443d-85b1-c0e79659ca0a"
},
"outputs": [],
- "source": ["from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef llm_fallback(state):\n \"\"\"\n Generate answer using the LLM w/o vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---LLM Fallback---\")\n question = state[\"question\"]\n generation = llm_chain.invoke({\"question\": question})\n return {\"question\": question, \"generation\": generation}\n\n\ndef generate(state):\n \"\"\"\n Generate answer using the vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n if not isinstance(documents, list):\n documents = [documents]\n\n # RAG generation\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n\n # Fallback to LLM or raise error if no decision\n if \"tool_calls\" not in source.additional_kwargs:\n print(\"---ROUTE QUESTION TO LLM---\")\n return \"llm_fallback\"\n if len(source.additional_kwargs[\"tool_calls\"]) == 0:\n raise \"Router could not decide source\"\n\n # Choose datasource\n datasource = source.additional_kwargs[\"tool_calls\"][0][\"function\"][\"name\"]\n if datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n else:\n print(\"---ROUTE QUESTION TO LLM---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, WEB SEARCH---\")\n return \"web_search\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
+ "source": [
+ "from langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef llm_fallback(state):\n \"\"\"\n Generate answer using the LLM w/o vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---LLM Fallback---\")\n question = state[\"question\"]\n generation = llm_chain.invoke({\"question\": question})\n return {\"question\": question, \"generation\": generation}\n\n\ndef generate(state):\n \"\"\"\n Generate answer using the vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n if not isinstance(documents, list):\n documents = [documents]\n\n # RAG generation\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n source = question_router.invoke({\"question\": question})\n\n # Fallback to LLM or raise error if no decision\n if \"tool_calls\" not in source.additional_kwargs:\n print(\"---ROUTE QUESTION TO LLM---\")\n return \"llm_fallback\"\n if len(source.additional_kwargs[\"tool_calls\"]) == 0:\n raise \"Router could not decide source\"\n\n # Choose datasource\n datasource = source.additional_kwargs[\"tool_calls\"][0][\"function\"][\"name\"]\n if datasource == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif datasource == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n else:\n print(\"---ROUTE QUESTION TO LLM---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, WEB SEARCH---\")\n return \"web_search\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score.binary_score\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score.binary_score\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""
+ ]
},
{
"cell_type": "markdown",
@@ -343,7 +369,54 @@
"id": "67854e07-9293-4c3c-bf9a-bc9a605570ee"
},
"outputs": [],
- "source": ["import pprint\n\nfrom langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"web_search\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # rag\nworkflow.add_node(\"llm_fallback\", llm_fallback) # llm\n\n# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"web_search\": \"web_search\",\n \"vectorstore\": \"retrieve\",\n \"llm_fallback\": \"llm_fallback\",\n })\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"web_search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\", # Hallucinations: re-generate\n \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search\n \"useful\": END,\n },\n)\nworkflow.add_edge(\"llm_fallback\", END)\n\n# Compile\napp = workflow.compile()"]
+ "source": [
+ "import pprint\n",
+ "\n",
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # rag\n",
+ "workflow.add_node(\"llm_fallback\", llm_fallback) # llm\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " \"llm_fallback\": \"llm_fallback\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\", # Hallucinations: re-generate\n",
+ " \"not useful\": \"web_search\", # Fails to answer question: fall-back to web-search\n",
+ " \"useful\": END,\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"llm_fallback\", END)\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
},
{
"cell_type": "code",
@@ -377,7 +450,9 @@
]
}
],
- "source": ["# Run\ninputs = {\n \"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"]
+ "source": [
+ "# Run\ninputs = {\n \"question\": \"What player are the Bears expected to draft first in the 2024 NFL draft?\"\n}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -432,7 +507,9 @@
]
}
],
- "source": ["# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"]
+ "source": [
+ "# Run\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -468,7 +545,9 @@
]
}
],
- "source": ["# Run\ninputs = {\"question\": \"Hello, how are you today?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"]
+ "source": [
+ "# Run\ninputs = {\"question\": \"Hello, how are you today?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint.pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")\n\n# Final generation\npprint.pprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -486,7 +565,9 @@
"id": "ce3cda0a-c4bd-41ea-830b-d992f27fde15",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/rag/langgraph_adaptive_rag_local.ipynb b/examples/rag/langgraph_adaptive_rag_local.ipynb
index fcbc27c8b..500efc649 100644
--- a/examples/rag/langgraph_adaptive_rag_local.ipynb
+++ b/examples/rag/langgraph_adaptive_rag_local.ipynb
@@ -44,7 +44,9 @@
"id": "88debf5c-6972-415c-b8fb-f65eab203b7a",
"metadata": {},
"outputs": [],
- "source": ["%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]"]
+ "source": [
+ "%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local]"
+ ]
},
{
"cell_type": "markdown",
@@ -76,7 +78,9 @@
"id": "af8379bd-7eae-4ba6-b632-12e89eab9920",
"metadata": {},
"outputs": [],
- "source": ["# Ollama model name\nlocal_llm = \"mistral\""]
+ "source": [
+ "# Ollama model name\nlocal_llm = \"mistral\""
+ ]
},
{
"cell_type": "markdown",
@@ -94,7 +98,9 @@
"id": "f6cb8dce-f580-421d-a05f-9fc71de2b023",
"metadata": {},
"outputs": [],
- "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""]
+ "source": [
+ "import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""
+ ]
},
{
"cell_type": "markdown",
@@ -110,7 +116,9 @@
"id": "f9ff6b99-080d-4827-b2cb-f775543d76f5",
"metadata": {},
"outputs": [],
- "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"]
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"
+ ]
},
{
"cell_type": "markdown",
@@ -136,7 +144,9 @@
]
}
],
- "source": ["### Router\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n You do not need to be stringent with the keywords in the question related to these topics. \\n\n Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n Question to route: {question}\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))"]
+ "source": [
+ "### Router\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are an expert at routing a user question to a vectorstore or web search. \\n\n Use the vectorstore for questions on LLM agents, prompt engineering, and adversarial attacks. \\n\n You do not need to be stringent with the keywords in the question related to these topics. \\n\n Otherwise, use web-search. Give a binary choice 'web_search' or 'vectorstore' based on the question. \\n\n Return the a JSON with a single key 'datasource' and no premable or explanation. \\n\n Question to route: {question}\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))"
+ ]
},
{
"cell_type": "code",
@@ -152,7 +162,9 @@
]
}
],
- "source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
+ "source": [
+ "### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keywords related to the user question, grade it as relevant. \\n\n It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
},
{
"cell_type": "code",
@@ -168,7 +180,9 @@
]
}
],
- "source": ["### Generate\n\nfrom langchain import hub\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
+ "source": [
+ "### Generate\n\nfrom langchain import hub\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = hub.pull(\"rlm/rag-prompt\")\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"
+ ]
},
{
"cell_type": "code",
@@ -187,7 +201,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
+ "source": [
+ "### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is grounded in / supported by a set of facts. \\n \n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation}\n Give a binary score 'yes' or 'no' score to indicate whether the answer is grounded in / supported by a set of facts. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -206,7 +222,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
+ "source": [
+ "### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a grader assessing whether an answer is useful to resolve a question. \\n \n Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question}\n Give a binary score 'yes' or 'no' to indicate whether the answer is useful to resolve a question. \\n\n Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -225,7 +243,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"]
+ "source": [
+ "### Question Re-writer\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Prompt\nre_write_prompt = PromptTemplate(\n template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nquestion_rewriter = re_write_prompt | llm | StrOutputParser()\nquestion_rewriter.invoke({\"question\": question})"
+ ]
},
{
"cell_type": "markdown",
@@ -241,7 +261,9 @@
"id": "6c3c1c70-ff84-41e8-bf72-738ed52f2dde",
"metadata": {},
"outputs": [],
- "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"]
+ "source": [
+ "### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"
+ ]
},
{
"cell_type": "markdown",
@@ -261,7 +283,9 @@
"id": "6e09087e-b2a9-437a-abee-129e426df799",
"metadata": {},
"outputs": [],
- "source": ["from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"]
+ "source": [
+ "from typing import List\n\nfrom typing_extensions import TypedDict\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n documents: List[str]"
+ ]
},
{
"cell_type": "code",
@@ -269,7 +293,9 @@
"id": "7c5fa507-77ae-426a-a65f-f518b9525bd0",
"metadata": {},
"outputs": [],
- "source": ["### Nodes\n\nfrom langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""]
+ "source": [
+ "### Nodes\n\nfrom langchain.schema import Document\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.get_relevant_documents(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n continue\n return {\"documents\": filtered_docs, \"question\": question}\n\n\ndef transform_query(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates question key with a re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Re-write question\n better_question = question_rewriter.invoke({\"question\": question})\n return {\"documents\": documents, \"question\": better_question}\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n\n return {\"documents\": web_results, \"question\": question}\n\n\n### Edges ###\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"web_search\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n filtered_documents = state[\"documents\"]\n\n if not filtered_documents:\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---\"\n )\n return \"transform_query\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\""
+ ]
},
{
"cell_type": "markdown",
@@ -285,7 +311,51 @@
"id": "450eb313-ca75-4a43-b57e-7034bd3f40bf",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.graph import END, StateGraph, START\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"web_search\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"transform_query\", transform_query) # transform_query\n\n# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"web_search\": \"web_search\",\n \"vectorstore\": \"retrieve\",\n })\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"transform_query\": \"transform_query\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"transform_query\", \"retrieve\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"transform_query\",\n },\n)\n\n# Compile\napp = workflow.compile()"]
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "workflow = StateGraph(GraphState)\n",
+ "\n",
+ "# Define the nodes\n",
+ "workflow.add_node(\"web_search\", web_search) # web search\n",
+ "workflow.add_node(\"retrieve\", retrieve) # retrieve\n",
+ "workflow.add_node(\"grade_documents\", grade_documents) # grade documents\n",
+ "workflow.add_node(\"generate\", generate) # generatae\n",
+ "workflow.add_node(\"transform_query\", transform_query) # transform_query\n",
+ "\n",
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"web_search\": \"web_search\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"web_search\", \"generate\")\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"transform_query\": \"transform_query\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"transform_query\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "# Compile\n",
+ "app = workflow.compile()"
+ ]
},
{
"cell_type": "code",
@@ -323,7 +393,9 @@
]
}
],
- "source": ["from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What is the AlphaCodium paper about?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"]
+ "source": [
+ "from pprint import pprint\n\n# Run\ninputs = {\"question\": \"What is the AlphaCodium paper about?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n # Node\n pprint(f\"Node '{key}':\")\n # Optional: print full state at each node\n # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n pprint(\"\\n---\\n\")\n\n# Final generation\npprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -341,7 +413,9 @@
"id": "4620ede9-b014-499f-8acf-80f80ce0d944",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/rag/langgraph_crag_local.ipynb b/examples/rag/langgraph_crag_local.ipynb
index adc218ac8..dbac6f697 100644
--- a/examples/rag/langgraph_crag_local.ipynb
+++ b/examples/rag/langgraph_crag_local.ipynb
@@ -56,7 +56,9 @@
"id": "4a660963-bd3d-4c87-b2e4-b6e432055211",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai"]
+ "source": [
+ "%%capture --no-stderr\n%pip install -U langchain_community tiktoken langchainhub scikit-learn langchain langgraph tavily-python nomic[local] langchain-nomic langchain_openai"
+ ]
},
{
"cell_type": "code",
@@ -64,7 +66,12 @@
"id": "68316ba0-854b-41e1-9af5-1f9e965946e3",
"metadata": {},
"outputs": [],
- "source": ["# Search\nimport os\nos.environ[\"TAVILY_API_KEY\"] = \"xxx\""]
+ "source": [
+ "# Search\n",
+ "import os\n",
+ "\n",
+ "os.environ[\"TAVILY_API_KEY\"] = \"xxx\""
+ ]
},
{
"cell_type": "code",
@@ -72,7 +79,9 @@
"id": "0be68860-dded-481e-9fc7-a5042bf92c04",
"metadata": {},
"outputs": [],
- "source": ["# Embedding (optional)\nos.environ[\"OPENAI_API_KEY\"] = \"xxx\""]
+ "source": [
+ "# Embedding (optional)\nos.environ[\"OPENAI_API_KEY\"] = \"xxx\""
+ ]
},
{
"cell_type": "code",
@@ -80,7 +89,9 @@
"id": "7248ab88-2b97-41eb-8dbb-4ea65525ed9a",
"metadata": {},
"outputs": [],
- "source": ["# Tracing and testing (optional)\nos.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\""]
+ "source": [
+ "# Tracing and testing (optional)\nos.environ[\"LANGCHAIN_API_KEY\"] = \"xxx\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"corrective-rag-agent-testing\""
+ ]
},
{
"cell_type": "markdown",
@@ -98,7 +109,9 @@
"id": "2f4db331-c4d0-4c7c-a9a5-0bebc8a89c6c",
"metadata": {},
"outputs": [],
- "source": ["local_llm = \"llama3\"\nmodel_tested = \"llama3-8b\"\nmetadata = f\"CRAG, {model_tested}\""]
+ "source": [
+ "local_llm = \"llama3\"\nmodel_tested = \"llama3-8b\"\nmetadata = f\"CRAG, {model_tested}\""
+ ]
},
{
"cell_type": "markdown",
@@ -124,7 +137,48 @@
]
}
],
- "source": ["from langchain.text_splitter import RecursiveCharacterTextSplitter\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_nomic.embeddings import NomicEmbeddings # local\nfrom langchain_openai import OpenAIEmbeddings # api\n\n# List of URLs to load documents from\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\n# Load documents from the URLs\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\n# Initialize a text splitter with specified chunk size and overlap\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\n\n# Split the documents into chunks\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Embedding\n'''\nembedding=NomicEmbeddings(\n model=\"nomic-embed-text-v1.5\",\n inference_mode=\"local\",\n)\n'''\nembedding = OpenAIEmbeddings()\n\n# Add the document chunks to the \"vector store\"\nvectorstore = SKLearnVectorStore.from_documents(\n documents=doc_splits,\n embedding=embedding,\n)\nretriever = vectorstore.as_retriever(k=4)"]
+ "source": [
+ "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
+ "from langchain_community.document_loaders import WebBaseLoader\n",
+ "from langchain_community.vectorstores import SKLearnVectorStore\n",
+ "from langchain_nomic.embeddings import NomicEmbeddings # local\n",
+ "from langchain_openai import OpenAIEmbeddings # api\n",
+ "\n",
+ "# List of URLs to load documents from\n",
+ "urls = [\n",
+ " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
+ " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
+ "]\n",
+ "\n",
+ "# Load documents from the URLs\n",
+ "docs = [WebBaseLoader(url).load() for url in urls]\n",
+ "docs_list = [item for sublist in docs for item in sublist]\n",
+ "\n",
+ "# Initialize a text splitter with specified chunk size and overlap\n",
+ "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
+ " chunk_size=250, chunk_overlap=0\n",
+ ")\n",
+ "\n",
+ "# Split the documents into chunks\n",
+ "doc_splits = text_splitter.split_documents(docs_list)\n",
+ "\n",
+ "# Embedding\n",
+ "\"\"\"\n",
+ "embedding=NomicEmbeddings(\n",
+ " model=\"nomic-embed-text-v1.5\",\n",
+ " inference_mode=\"local\",\n",
+ ")\n",
+ "\"\"\"\n",
+ "embedding = OpenAIEmbeddings()\n",
+ "\n",
+ "# Add the document chunks to the \"vector store\"\n",
+ "vectorstore = SKLearnVectorStore.from_documents(\n",
+ " documents=doc_splits,\n",
+ " embedding=embedding,\n",
+ ")\n",
+ "retriever = vectorstore.as_retriever(k=4)"
+ ]
},
{
"attachments": {},
@@ -149,7 +203,9 @@
]
}
],
- "source": ["### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_mistralai.chat_models import ChatMistralAI\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a teacher grading a quiz. You will be given: \n 1/ a QUESTION\n 2/ A FACT provided by the student\n \n You are grading RELEVANCE RECALL:\n A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n 1 is the highest (best) score. 0 is the lowest score you can give. \n \n Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n \n Avoid simply stating the correct answer at the outset.\n \n Question: {question} \\n\n Fact: \\n\\n {documents} \\n\\n\n \n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))"]
+ "source": [
+ "### Retrieval Grader\n\nfrom langchain.prompts import PromptTemplate\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_mistralai.chat_models import ChatMistralAI\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are a teacher grading a quiz. You will be given: \n 1/ a QUESTION\n 2/ A FACT provided by the student\n \n You are grading RELEVANCE RECALL:\n A score of 1 means that ANY of the statements in the FACT are relevant to the QUESTION. \n A score of 0 means that NONE of the statements in the FACT are relevant to the QUESTION. \n 1 is the highest (best) score. 0 is the lowest score you can give. \n \n Explain your reasoning in a step-by-step manner. Ensure your reasoning and conclusion are correct. \n \n Avoid simply stating the correct answer at the outset.\n \n Question: {question} \\n\n Fact: \\n\\n {documents} \\n\\n\n \n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"documents\": doc_txt}))"
+ ]
},
{
"cell_type": "code",
@@ -165,7 +221,9 @@
]
}
],
- "source": ["### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are an assistant for question-answering tasks. \n \n Use the following documents to answer the question. \n \n If you don't know the answer, just say that you don't know. \n \n Use three sentences maximum and keep the answer concise:\n Question: {question} \n Documents: {documents} \n Answer: \n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"]
+ "source": [
+ "### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"You are an assistant for question-answering tasks. \n \n Use the following documents to answer the question. \n \n If you don't know the answer, just say that you don't know. \n \n Use three sentences maximum and keep the answer concise:\n Question: {question} \n Documents: {documents} \n Answer: \n \"\"\",\n input_variables=[\"question\", \"documents\"],\n)\n\n# LLM\nllm = ChatOllama(model=local_llm, temperature=0)\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\ngeneration = rag_chain.invoke({\"documents\": docs, \"question\": question})\nprint(generation)"
+ ]
},
{
"cell_type": "code",
@@ -173,7 +231,9 @@
"id": "b36a2f36-bc5f-408d-a5e8-3fa203c233f6",
"metadata": {},
"outputs": [],
- "source": ["### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"]
+ "source": [
+ "### Search\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"
+ ]
},
{
"cell_type": "markdown",
@@ -202,7 +262,9 @@
"output_type": "display_data"
}
],
- "source": ["from typing import List\nfrom typing_extensions import TypedDict\nfrom IPython.display import Image, display\nfrom langchain.schema import Document\nfrom langgraph.graph import START, END, StateGraph\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n search: str\n documents: List[str]\n steps: List[str]\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n question = state[\"question\"]\n documents = retriever.invoke(question)\n steps = state[\"steps\"]\n steps.append(\"retrieve_documents\")\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n steps = state[\"steps\"]\n steps.append(\"generate_answer\")\n return {\n \"documents\": documents,\n \"question\": question,\n \"generation\": generation,\n \"steps\": steps,\n }\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n steps = state[\"steps\"]\n steps.append(\"grade_document_retrieval\")\n filtered_docs = []\n search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"documents\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n filtered_docs.append(d)\n else:\n search = \"Yes\"\n continue\n return {\n \"documents\": filtered_docs,\n \"question\": question,\n \"search\": search,\n \"steps\": steps,\n }\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n question = state[\"question\"]\n documents = state.get(\"documents\", [])\n steps = state[\"steps\"]\n steps.append(\"web_search\")\n web_results = web_search_tool.invoke({\"query\": question})\n documents.extend(\n [\n Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n for d in web_results\n ]\n )\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n search = state[\"search\"]\n if search == \"Yes\":\n return \"search\"\n else:\n return \"generate\"\n\n\n# Graph\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"web_search\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\ncustom_graph = workflow.compile()\n\ndisplay(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))"]
+ "source": [
+ "from typing import List\nfrom typing_extensions import TypedDict\nfrom IPython.display import Image, display\nfrom langchain.schema import Document\nfrom langgraph.graph import START, END, StateGraph\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n search: str\n documents: List[str]\n steps: List[str]\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n question = state[\"question\"]\n documents = retriever.invoke(question)\n steps = state[\"steps\"]\n steps.append(\"retrieve_documents\")\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = rag_chain.invoke({\"documents\": documents, \"question\": question})\n steps = state[\"steps\"]\n steps.append(\"generate_answer\")\n return {\n \"documents\": documents,\n \"question\": question,\n \"generation\": generation,\n \"steps\": steps,\n }\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with only filtered relevant documents\n \"\"\"\n\n question = state[\"question\"]\n documents = state[\"documents\"]\n steps = state[\"steps\"]\n steps.append(\"grade_document_retrieval\")\n filtered_docs = []\n search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"documents\": d.page_content}\n )\n grade = score[\"score\"]\n if grade == \"yes\":\n filtered_docs.append(d)\n else:\n search = \"Yes\"\n continue\n return {\n \"documents\": filtered_docs,\n \"question\": question,\n \"search\": search,\n \"steps\": steps,\n }\n\n\ndef web_search(state):\n \"\"\"\n Web search based on the re-phrased question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Updates documents key with appended web results\n \"\"\"\n\n question = state[\"question\"]\n documents = state.get(\"documents\", [])\n steps = state[\"steps\"]\n steps.append(\"web_search\")\n web_results = web_search_tool.invoke({\"query\": question})\n documents.extend(\n [\n Document(page_content=d[\"content\"], metadata={\"url\": d[\"url\"]})\n for d in web_results\n ]\n )\n return {\"documents\": documents, \"question\": question, \"steps\": steps}\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or re-generate a question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n search = state[\"search\"]\n if search == \"Yes\":\n return \"search\"\n else:\n return \"generate\"\n\n\n# Graph\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae\nworkflow.add_node(\"web_search\", web_search) # web search\n\n# Build graph\nworkflow.add_edge(START, \"retrieve\")\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"search\": \"web_search\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"web_search\", \"generate\")\nworkflow.add_edge(\"generate\", END)\n\ncustom_graph = workflow.compile()\n\ndisplay(Image(custom_graph.get_graph(xray=True).draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -225,7 +287,22 @@
"output_type": "execute_result"
}
],
- "source": ["import uuid\n\ndef predict_custom_agent_local_answer(example: dict):\n config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n state_dict = custom_graph.invoke(\n {\"question\": example[\"input\"], \"steps\": []}, config\n )\n return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n\n\nexample = {\"input\": \"What are the types of agent memory?\"}\nresponse = predict_custom_agent_local_answer(example)\nresponse"]
+ "source": [
+ "import uuid\n",
+ "\n",
+ "\n",
+ "def predict_custom_agent_local_answer(example: dict):\n",
+ " config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
+ " state_dict = custom_graph.invoke(\n",
+ " {\"question\": example[\"input\"], \"steps\": []}, config\n",
+ " )\n",
+ " return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n",
+ "\n",
+ "\n",
+ "example = {\"input\": \"What are the types of agent memory?\"}\n",
+ "response = predict_custom_agent_local_answer(example)\n",
+ "response"
+ ]
},
{
"cell_type": "markdown",
@@ -261,7 +338,9 @@
"id": "b83706ac-724b-46b1-9f08-66e6c4fac742",
"metadata": {},
"outputs": [],
- "source": ["from langsmith import Client\n\nclient = Client()\n\n# Create a dataset\nexamples = [\n (\n \"How does the ReAct agent use self-reflection? \",\n \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n ),\n (\n \"What are the types of biases that can arise with few-shot prompting?\",\n \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n ),\n (\n \"What are five types of adversarial attacks?\",\n \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n ),\n (\n \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n ),\n (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n]\n\n# Save it\ndataset_name = \"Corrective RAG Agent Testing\"\nif not client.has_dataset(dataset_name=dataset_name):\n dataset = client.create_dataset(dataset_name=dataset_name)\n inputs, outputs = zip(\n *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n )\n client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)"]
+ "source": [
+ "from langsmith import Client\n\nclient = Client()\n\n# Create a dataset\nexamples = [\n (\n \"How does the ReAct agent use self-reflection? \",\n \"ReAct integrates reasoning and acting, performing actions - such tools like Wikipedia search API - and then observing / reasoning about the tool outputs.\",\n ),\n (\n \"What are the types of biases that can arise with few-shot prompting?\",\n \"The biases that can arise with few-shot prompting include (1) Majority label bias, (2) Recency bias, and (3) Common token bias.\",\n ),\n (\n \"What are five types of adversarial attacks?\",\n \"Five types of adversarial attacks are (1) Token manipulation, (2) Gradient based attack, (3) Jailbreak prompting, (4) Human red-teaming, (5) Model red-teaming.\",\n ),\n (\n \"Who did the Chicago Bears draft first in the 2024 NFL draft”?\",\n \"The Chicago Bears drafted Caleb Williams first in the 2024 NFL draft.\",\n ),\n (\"Who won the 2024 NBA finals?\", \"The Boston Celtics on the 2024 NBA finals\"),\n]\n\n# Save it\ndataset_name = \"Corrective RAG Agent Testing\"\nif not client.has_dataset(dataset_name=dataset_name):\n dataset = client.create_dataset(dataset_name=dataset_name)\n inputs, outputs = zip(\n *[({\"input\": text}, {\"output\": label}) for text, label in examples]\n )\n client.create_examples(inputs=inputs, outputs=outputs, dataset_id=dataset.id)"
+ ]
},
{
"cell_type": "markdown",
@@ -281,7 +360,39 @@
"id": "0a63776c-f9cd-46ce-b8cf-95c066dc5b06",
"metadata": {},
"outputs": [],
- "source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\n# Grade prompt\ngrade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n\ndef answer_evaluator(run, example) -> dict:\n \"\"\"\n A simple evaluator for RAG answer accuracy\n \"\"\"\n\n # Get the question, the ground truth reference answer, RAG chain answer prediction\n input_question = example.inputs[\"input\"]\n reference = example.outputs[\"output\"]\n prediction = run.outputs[\"response\"]\n\n # Define an LLM grader\n llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n answer_grader = grade_prompt_answer_accuracy | llm\n\n # Run evaluator\n score = answer_grader.invoke(\n {\n \"question\": input_question,\n \"correct_answer\": reference,\n \"student_answer\": prediction,\n }\n )\n score = score[\"Score\"]\n return {\"key\": \"answer_v_reference_score\", \"score\": score}"]
+ "source": [
+ "from langchain import hub\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "# Grade prompt\n",
+ "grade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n",
+ "\n",
+ "\n",
+ "def answer_evaluator(run, example) -> dict:\n",
+ " \"\"\"\n",
+ " A simple evaluator for RAG answer accuracy\n",
+ " \"\"\"\n",
+ "\n",
+ " # Get the question, the ground truth reference answer, RAG chain answer prediction\n",
+ " input_question = example.inputs[\"input\"]\n",
+ " reference = example.outputs[\"output\"]\n",
+ " prediction = run.outputs[\"response\"]\n",
+ "\n",
+ " # Define an LLM grader\n",
+ " llm = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
+ " answer_grader = grade_prompt_answer_accuracy | llm\n",
+ "\n",
+ " # Run evaluator\n",
+ " score = answer_grader.invoke(\n",
+ " {\n",
+ " \"question\": input_question,\n",
+ " \"correct_answer\": reference,\n",
+ " \"student_answer\": prediction,\n",
+ " }\n",
+ " )\n",
+ " score = score[\"Score\"]\n",
+ " return {\"key\": \"answer_v_reference_score\", \"score\": score}"
+ ]
},
{
"cell_type": "markdown",
@@ -301,7 +412,51 @@
"id": "deb28175-27a1-4afc-9747-2983e87fc881",
"metadata": {},
"outputs": [],
- "source": ["from langsmith.schemas import Example, Run\n\n# Reasoning traces that we expect the agents to take\nexpected_trajectory_1 = [\n \"retrieve_documents\",\n \"grade_document_retrieval\",\n \"web_search\",\n \"generate_answer\",\n]\nexpected_trajectory_2 = [\n \"retrieve_documents\",\n \"grade_document_retrieval\",\n \"generate_answer\",\n]\n\ndef check_trajectory_react(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n messages = root_run.outputs[\"messages\"]\n tool_calls = find_tool_calls_react(messages)\n print(f\"Tool calls ReAct agent: {tool_calls}\")\n if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n\n\ndef check_trajectory_custom(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n tool_calls = root_run.outputs[\"steps\"]\n print(f\"Tool calls custom agent: {tool_calls}\")\n if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}"]
+ "source": [
+ "from langsmith.schemas import Example, Run\n",
+ "\n",
+ "# Reasoning traces that we expect the agents to take\n",
+ "expected_trajectory_1 = [\n",
+ " \"retrieve_documents\",\n",
+ " \"grade_document_retrieval\",\n",
+ " \"web_search\",\n",
+ " \"generate_answer\",\n",
+ "]\n",
+ "expected_trajectory_2 = [\n",
+ " \"retrieve_documents\",\n",
+ " \"grade_document_retrieval\",\n",
+ " \"generate_answer\",\n",
+ "]\n",
+ "\n",
+ "\n",
+ "def check_trajectory_react(root_run: Run, example: Example) -> dict:\n",
+ " \"\"\"\n",
+ " Check if all expected tools are called in exact order and without any additional tool calls.\n",
+ " \"\"\"\n",
+ " messages = root_run.outputs[\"messages\"]\n",
+ " tool_calls = find_tool_calls_react(messages)\n",
+ " print(f\"Tool calls ReAct agent: {tool_calls}\")\n",
+ " if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n",
+ " score = 1\n",
+ " else:\n",
+ " score = 0\n",
+ "\n",
+ " return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}\n",
+ "\n",
+ "\n",
+ "def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n",
+ " \"\"\"\n",
+ " Check if all expected tools are called in exact order and without any additional tool calls.\n",
+ " \"\"\"\n",
+ " tool_calls = root_run.outputs[\"steps\"]\n",
+ " print(f\"Tool calls custom agent: {tool_calls}\")\n",
+ " if tool_calls == expected_trajectory_1 or tool_calls == expected_trajectory_2:\n",
+ " score = 1\n",
+ " else:\n",
+ " score = 0\n",
+ "\n",
+ " return {\"score\": int(score), \"key\": \"tool_calls_in_exact_order\"}"
+ ]
},
{
"cell_type": "code",
@@ -355,7 +510,20 @@
]
}
],
- "source": ["from langsmith.evaluation import evaluate\n\nexperiment_prefix = f\"custom-agent-{model_tested}\"\nexperiment_results = evaluate(\n predict_custom_agent_local_answer,\n data=dataset_name,\n evaluators=[answer_evaluator, check_trajectory_custom],\n experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n num_repetitions=3,\n max_concurrency=1, # Use when running locally\n metadata={\"version\": metadata},\n)"]
+ "source": [
+ "from langsmith.evaluation import evaluate\n",
+ "\n",
+ "experiment_prefix = f\"custom-agent-{model_tested}\"\n",
+ "experiment_results = evaluate(\n",
+ " predict_custom_agent_local_answer,\n",
+ " data=dataset_name,\n",
+ " evaluators=[answer_evaluator, check_trajectory_custom],\n",
+ " experiment_prefix=experiment_prefix + \"-answer-and-tool-use\",\n",
+ " num_repetitions=3,\n",
+ " max_concurrency=1, # Use when running locally\n",
+ " metadata={\"version\": metadata},\n",
+ ")"
+ ]
},
{
"attachments": {
@@ -382,7 +550,9 @@
"id": "79295798-0181-417e-abad-11dddb6ff05e",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/rag/langgraph_rag_agent_llama3_local.ipynb b/examples/rag/langgraph_rag_agent_llama3_local.ipynb
index 2046cea6e..5a9df7a41 100644
--- a/examples/rag/langgraph_rag_agent_llama3_local.ipynb
+++ b/examples/rag/langgraph_rag_agent_llama3_local.ipynb
@@ -49,7 +49,9 @@
"id": "21e597f9",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local] langchain-text-splitters"]
+ "source": [
+ "%%capture --no-stderr\n%pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python nomic[local] langchain-text-splitters"
+ ]
},
{
"cell_type": "markdown",
@@ -65,7 +67,9 @@
"id": "333cbcf4",
"metadata": {},
"outputs": [],
- "source": ["import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""]
+ "source": [
+ "import os\n\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_ENDPOINT\"] = \"https://api.smith.langchain.com\"\nos.environ[\"LANGCHAIN_API_KEY\"] = \"\""
+ ]
},
{
"cell_type": "code",
@@ -73,7 +77,9 @@
"id": "2096d49c-d3dc-4329-ada7-aff56d210198",
"metadata": {},
"outputs": [],
- "source": ["### LLM\n\nlocal_llm = \"llama3\""]
+ "source": [
+ "### LLM\n\nlocal_llm = \"llama3\""
+ ]
},
{
"cell_type": "code",
@@ -81,7 +87,9 @@
"id": "267c63e1-4c2f-439d-8d95-4c6aa01f41cf",
"metadata": {},
"outputs": [],
- "source": ["### Index\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"]
+ "source": [
+ "### Index\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_nomic.embeddings import NomicEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=250, chunk_overlap=0\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n)\nretriever = vectorstore.as_retriever()"
+ ]
},
{
"cell_type": "code",
@@ -97,7 +105,9 @@
]
}
],
- "source": ["### Retrieval Grader\n\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing relevance \n of a retrieved document to a user question. If the document contains keywords related to the user question, \n grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n <|eot_id|><|start_header_id|>user<|end_header_id|>\n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n \"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"]
+ "source": [
+ "### Retrieval Grader\n\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing relevance \n of a retrieved document to a user question. If the document contains keywords related to the user question, \n grade it as relevant. It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\n <|eot_id|><|start_header_id|>user<|end_header_id|>\n Here is the retrieved document: \\n\\n {document} \\n\\n\n Here is the user question: {question} \\n <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n \"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nretrieval_grader = prompt | llm | JsonOutputParser()\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ndoc_txt = docs[1].page_content\nprint(retrieval_grader.invoke({\"question\": question, \"document\": doc_txt}))"
+ ]
},
{
"cell_type": "code",
@@ -113,7 +123,9 @@
]
}
],
- "source": ["### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an assistant for question-answering tasks. \n Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. \n Use three sentences maximum and keep the answer concise <|eot_id|><|start_header_id|>user<|end_header_id|>\n Question: {question} \n Context: {context} \n Answer: <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"]
+ "source": [
+ "### Generate\n\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an assistant for question-answering tasks. \n Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. \n Use three sentences maximum and keep the answer concise <|eot_id|><|start_header_id|>user<|end_header_id|>\n Question: {question} \n Context: {context} \n Answer: <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"question\", \"document\"],\n)\n\nllm = ChatOllama(model=local_llm, temperature=0)\n\n\n# Post-processing\ndef format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n\n# Chain\nrag_chain = prompt | llm | StrOutputParser()\n\n# Run\nquestion = \"agent memory\"\ndocs = retriever.invoke(question)\ngeneration = rag_chain.invoke({\"context\": docs, \"question\": question})\nprint(generation)"
+ ]
},
{
"cell_type": "code",
@@ -132,7 +144,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\" <|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether \n an answer is grounded in / supported by a set of facts. Give a binary 'yes' or 'no' score to indicate \n whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a \n single key 'score' and no preamble or explanation. <|eot_id|><|start_header_id|>user<|end_header_id|>\n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"]
+ "source": [
+ "### Hallucination Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\" <|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether \n an answer is grounded in / supported by a set of facts. Give a binary 'yes' or 'no' score to indicate \n whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a \n single key 'score' and no preamble or explanation. <|eot_id|><|start_header_id|>user<|end_header_id|>\n Here are the facts:\n \\n ------- \\n\n {documents} \n \\n ------- \\n\n Here is the answer: {generation} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"generation\", \"documents\"],\n)\n\nhallucination_grader = prompt | llm | JsonOutputParser()\nhallucination_grader.invoke({\"documents\": docs, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -151,7 +165,9 @@
"output_type": "execute_result"
}
],
- "source": ["### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether an \n answer is useful to resolve a question. Give a binary score 'yes' or 'no' to indicate whether the answer is \n useful to resolve a question. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\n <|eot_id|><|start_header_id|>user<|end_header_id|> Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"]
+ "source": [
+ "### Answer Grader\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\n# Prompt\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether an \n answer is useful to resolve a question. Give a binary score 'yes' or 'no' to indicate whether the answer is \n useful to resolve a question. Provide the binary score as a JSON with a single key 'score' and no preamble or explanation.\n <|eot_id|><|start_header_id|>user<|end_header_id|> Here is the answer:\n \\n ------- \\n\n {generation} \n \\n ------- \\n\n Here is the question: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"generation\", \"question\"],\n)\n\nanswer_grader = prompt | llm | JsonOutputParser()\nanswer_grader.invoke({\"question\": question, \"generation\": generation})"
+ ]
},
{
"cell_type": "code",
@@ -167,7 +183,9 @@
]
}
],
- "source": ["### Router\n\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an expert at routing a \n user question to a vectorstore or web search. Use the vectorstore for questions on LLM agents, \n prompt engineering, and adversarial attacks. You do not need to be stringent with the keywords \n in the question related to these topics. Otherwise, use web-search. Give a binary choice 'web_search' \n or 'vectorstore' based on the question. Return the a JSON with a single key 'datasource' and \n no premable or explanation. Question to route: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))"]
+ "source": [
+ "### Router\n\nfrom langchain_community.chat_models import ChatOllama\nfrom langchain_core.output_parsers import JsonOutputParser\nfrom langchain_core.prompts import PromptTemplate\n\n# LLM\nllm = ChatOllama(model=local_llm, format=\"json\", temperature=0)\n\nprompt = PromptTemplate(\n template=\"\"\"<|begin_of_text|><|start_header_id|>system<|end_header_id|> You are an expert at routing a \n user question to a vectorstore or web search. Use the vectorstore for questions on LLM agents, \n prompt engineering, and adversarial attacks. You do not need to be stringent with the keywords \n in the question related to these topics. Otherwise, use web-search. Give a binary choice 'web_search' \n or 'vectorstore' based on the question. Return the a JSON with a single key 'datasource' and \n no premable or explanation. Question to route: {question} <|eot_id|><|start_header_id|>assistant<|end_header_id|>\"\"\",\n input_variables=[\"question\"],\n)\n\nquestion_router = prompt | llm | JsonOutputParser()\nquestion = \"llm agent memory\"\ndocs = retriever.get_relevant_documents(question)\ndoc_txt = docs[1].page_content\nprint(question_router.invoke({\"question\": question}))"
+ ]
},
{
"cell_type": "code",
@@ -175,7 +193,9 @@
"id": "023ff2db-eb4e-4d44-904c-ea061abc16d9",
"metadata": {},
"outputs": [],
- "source": ["### Search\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"]
+ "source": [
+ "### Search\nfrom langchain_community.tools.tavily_search import TavilySearchResults\n\nweb_search_tool = TavilySearchResults(k=3)"
+ ]
},
{
"cell_type": "markdown",
@@ -191,7 +211,9 @@
"id": "07fa3d08-6a86-4705-a28b-e2721070bc5e",
"metadata": {},
"outputs": [],
- "source": ["from pprint import pprint\nfrom typing import List\n\nfrom langchain_core.documents import Document\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n### State\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n web_search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n web_search: str\n documents: List[str]\n\n\n### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents from vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer using RAG on retrieved documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question\n If any document is not relevant, we will set a flag to run web search\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Filtered out irrelevant documents and updated web_search state\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n web_search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n # Document relevant\n if grade.lower() == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n # Document not relevant\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n # We do not include the document in filtered_docs\n # We set a flag to indicate that we want to run web search\n web_search = \"Yes\"\n continue\n return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n\n\ndef web_search(state):\n \"\"\"\n Web search based based on the question\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Appended web results to documents\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n if documents is not None:\n documents.append(web_results)\n else:\n documents = [web_results]\n return {\"documents\": documents, \"question\": question}\n\n\n### Conditional edge\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"websearch\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or add web search\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n web_search = state[\"web_search\"]\n state[\"documents\"]\n\n if web_search == \"Yes\":\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n )\n return \"websearch\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\n### Conditional edge\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\"\n\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"websearch\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae"]
+ "source": [
+ "from pprint import pprint\nfrom typing import List\n\nfrom langchain_core.documents import Document\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n### State\n\n\nclass GraphState(TypedDict):\n \"\"\"\n Represents the state of our graph.\n\n Attributes:\n question: question\n generation: LLM generation\n web_search: whether to add search\n documents: list of documents\n \"\"\"\n\n question: str\n generation: str\n web_search: str\n documents: List[str]\n\n\n### Nodes\n\n\ndef retrieve(state):\n \"\"\"\n Retrieve documents from vectorstore\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, documents, that contains retrieved documents\n \"\"\"\n print(\"---RETRIEVE---\")\n question = state[\"question\"]\n\n # Retrieval\n documents = retriever.invoke(question)\n return {\"documents\": documents, \"question\": question}\n\n\ndef generate(state):\n \"\"\"\n Generate answer using RAG on retrieved documents\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): New key added to state, generation, that contains LLM generation\n \"\"\"\n print(\"---GENERATE---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # RAG generation\n generation = rag_chain.invoke({\"context\": documents, \"question\": question})\n return {\"documents\": documents, \"question\": question, \"generation\": generation}\n\n\ndef grade_documents(state):\n \"\"\"\n Determines whether the retrieved documents are relevant to the question\n If any document is not relevant, we will set a flag to run web search\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Filtered out irrelevant documents and updated web_search state\n \"\"\"\n\n print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Score each doc\n filtered_docs = []\n web_search = \"No\"\n for d in documents:\n score = retrieval_grader.invoke(\n {\"question\": question, \"document\": d.page_content}\n )\n grade = score[\"score\"]\n # Document relevant\n if grade.lower() == \"yes\":\n print(\"---GRADE: DOCUMENT RELEVANT---\")\n filtered_docs.append(d)\n # Document not relevant\n else:\n print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n # We do not include the document in filtered_docs\n # We set a flag to indicate that we want to run web search\n web_search = \"Yes\"\n continue\n return {\"documents\": filtered_docs, \"question\": question, \"web_search\": web_search}\n\n\ndef web_search(state):\n \"\"\"\n Web search based based on the question\n\n Args:\n state (dict): The current graph state\n\n Returns:\n state (dict): Appended web results to documents\n \"\"\"\n\n print(\"---WEB SEARCH---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n\n # Web search\n docs = web_search_tool.invoke({\"query\": question})\n web_results = \"\\n\".join([d[\"content\"] for d in docs])\n web_results = Document(page_content=web_results)\n if documents is not None:\n documents.append(web_results)\n else:\n documents = [web_results]\n return {\"documents\": documents, \"question\": question}\n\n\n### Conditional edge\n\n\ndef route_question(state):\n \"\"\"\n Route question to web search or RAG.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Next node to call\n \"\"\"\n\n print(\"---ROUTE QUESTION---\")\n question = state[\"question\"]\n print(question)\n source = question_router.invoke({\"question\": question})\n print(source)\n print(source[\"datasource\"])\n if source[\"datasource\"] == \"web_search\":\n print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n return \"websearch\"\n elif source[\"datasource\"] == \"vectorstore\":\n print(\"---ROUTE QUESTION TO RAG---\")\n return \"vectorstore\"\n\n\ndef decide_to_generate(state):\n \"\"\"\n Determines whether to generate an answer, or add web search\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Binary decision for next node to call\n \"\"\"\n\n print(\"---ASSESS GRADED DOCUMENTS---\")\n state[\"question\"]\n web_search = state[\"web_search\"]\n state[\"documents\"]\n\n if web_search == \"Yes\":\n # All documents have been filtered check_relevance\n # We will re-generate a new query\n print(\n \"---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, INCLUDE WEB SEARCH---\"\n )\n return \"websearch\"\n else:\n # We have relevant documents, so generate answer\n print(\"---DECISION: GENERATE---\")\n return \"generate\"\n\n\n### Conditional edge\n\n\ndef grade_generation_v_documents_and_question(state):\n \"\"\"\n Determines whether the generation is grounded in the document and answers question.\n\n Args:\n state (dict): The current graph state\n\n Returns:\n str: Decision for next node to call\n \"\"\"\n\n print(\"---CHECK HALLUCINATIONS---\")\n question = state[\"question\"]\n documents = state[\"documents\"]\n generation = state[\"generation\"]\n\n score = hallucination_grader.invoke(\n {\"documents\": documents, \"generation\": generation}\n )\n grade = score[\"score\"]\n\n # Check hallucination\n if grade == \"yes\":\n print(\"---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---\")\n # Check question-answering\n print(\"---GRADE GENERATION vs QUESTION---\")\n score = answer_grader.invoke({\"question\": question, \"generation\": generation})\n grade = score[\"score\"]\n if grade == \"yes\":\n print(\"---DECISION: GENERATION ADDRESSES QUESTION---\")\n return \"useful\"\n else:\n print(\"---DECISION: GENERATION DOES NOT ADDRESS QUESTION---\")\n return \"not useful\"\n else:\n pprint(\"---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---\")\n return \"not supported\"\n\n\nworkflow = StateGraph(GraphState)\n\n# Define the nodes\nworkflow.add_node(\"websearch\", web_search) # web search\nworkflow.add_node(\"retrieve\", retrieve) # retrieve\nworkflow.add_node(\"grade_documents\", grade_documents) # grade documents\nworkflow.add_node(\"generate\", generate) # generatae"
+ ]
},
{
"cell_type": "markdown",
@@ -207,7 +229,37 @@
"id": "d9a4b9e4-3ba8-47d6-958c-e5a7112ac6f4",
"metadata": {},
"outputs": [],
- "source": ["# Build graph\nworkflow.add_conditional_edges(START, route_question,\n {\n \"websearch\": \"websearch\",\n \"vectorstore\": \"retrieve\",\n })\n\nworkflow.add_edge(\"retrieve\", \"grade_documents\")\nworkflow.add_conditional_edges(\n \"grade_documents\",\n decide_to_generate,\n {\n \"websearch\": \"websearch\",\n \"generate\": \"generate\",\n },\n)\nworkflow.add_edge(\"websearch\", \"generate\")\nworkflow.add_conditional_edges(\n \"generate\",\n grade_generation_v_documents_and_question,\n {\n \"not supported\": \"generate\",\n \"useful\": END,\n \"not useful\": \"websearch\",\n },\n)"]
+ "source": [
+ "# Build graph\n",
+ "workflow.add_conditional_edges(\n",
+ " START,\n",
+ " route_question,\n",
+ " {\n",
+ " \"websearch\": \"websearch\",\n",
+ " \"vectorstore\": \"retrieve\",\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"grade_documents\",\n",
+ " decide_to_generate,\n",
+ " {\n",
+ " \"websearch\": \"websearch\",\n",
+ " \"generate\": \"generate\",\n",
+ " },\n",
+ ")\n",
+ "workflow.add_edge(\"websearch\", \"generate\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"generate\",\n",
+ " grade_generation_v_documents_and_question,\n",
+ " {\n",
+ " \"not supported\": \"generate\",\n",
+ " \"useful\": END,\n",
+ " \"not useful\": \"websearch\",\n",
+ " },\n",
+ ")"
+ ]
},
{
"cell_type": "code",
@@ -254,7 +306,9 @@
]
}
],
- "source": ["# Compile\napp = workflow.compile()\n\n# Test\n\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n pprint(f\"Finished running: {key}:\")\npprint(value[\"generation\"])"]
+ "source": [
+ "# Compile\napp = workflow.compile()\n\n# Test\n\ninputs = {\"question\": \"What are the types of agent memory?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n pprint(f\"Finished running: {key}:\")\npprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -294,7 +348,9 @@
]
}
],
- "source": ["from pprint import pprint\n\n# Compile\napp = workflow.compile()\ninputs = {\"question\": \"Who are the Bears expected to draft first in the NFL draft?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n pprint(f\"Finished running: {key}:\")\npprint(value[\"generation\"])"]
+ "source": [
+ "from pprint import pprint\n\n# Compile\napp = workflow.compile()\ninputs = {\"question\": \"Who are the Bears expected to draft first in the NFL draft?\"}\nfor output in app.stream(inputs):\n for key, value in output.items():\n pprint(f\"Finished running: {key}:\")\npprint(value[\"generation\"])"
+ ]
},
{
"cell_type": "markdown",
@@ -312,7 +368,9 @@
"id": "1da64a95-736b-4373-8fb4-6ed4bf60a647",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": [
+ ""
+ ]
}
],
"metadata": {
diff --git a/examples/state-context-key.ipynb b/examples/state-context-key.ipynb
index 5a96d2ef0..3b6191fe6 100644
--- a/examples/state-context-key.ipynb
+++ b/examples/state-context-key.ipynb
@@ -210,12 +210,14 @@
"from langchain_core.pydantic_v1 import BaseModel\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
+ "\n",
"class AgentContext(BaseModel):\n",
" class Config:\n",
" arbitrary_types_allowed = True\n",
"\n",
" httpx_session: httpx.Client\n",
"\n",
+ "\n",
"@contextmanager\n",
"def make_agent_context(config: RunnableConfig):\n",
" # here you could read the config values passed invoke/stream to customize the context object\n",
@@ -225,7 +227,7 @@
" try:\n",
" yield AgentContext(httpx_session=session)\n",
" finally:\n",
- " session.close()\n"
+ " session.close()"
]
},
{
@@ -324,9 +326,9 @@
"# Define the function that calls the model\n",
"def call_model(state):\n",
" # using context value\n",
- " req = state.context.httpx_session.get('https://www.langchain.com/')\n",
+ " req = state.context.httpx_session.get(\"https://www.langchain.com/\")\n",
" assert req.status_code == 200, req\n",
- " \n",
+ "\n",
" messages = state.messages\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
diff --git a/examples/state-model.ipynb b/examples/state-model.ipynb
index afc94f45c..897c3c571 100644
--- a/examples/state-model.ipynb
+++ b/examples/state-model.ipynb
@@ -32,7 +32,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",
@@ -48,7 +51,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",
@@ -64,7 +78,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",
@@ -84,7 +101,20 @@
"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 [\"The answer to your question lies within.\"]\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 [\"The answer to your question lies within.\"]\n",
+ "\n",
+ "\n",
+ "tools = [search]"
+ ]
},
{
"cell_type": "markdown",
@@ -103,7 +133,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",
@@ -127,7 +161,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",
@@ -145,7 +183,9 @@
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
"metadata": {},
"outputs": [],
- "source": ["model = model.bind_tools(tools)"]
+ "source": [
+ "model = model.bind_tools(tools)"
+ ]
},
{
"cell_type": "markdown",
@@ -171,7 +211,17 @@
"id": "ea793afa-2eab-4901-910d-6eed90cd6564",
"metadata": {},
"outputs": [],
- "source": ["import operator\nfrom typing import Annotated, Sequence\n\nfrom langchain_core.messages import BaseMessage\nfrom langchain_core.pydantic_v1 import BaseModel\n\n\nclass AgentState(BaseModel):\n messages: Annotated[Sequence[BaseMessage], operator.add]"]
+ "source": [
+ "import operator\n",
+ "from typing import Annotated, Sequence\n",
+ "\n",
+ "from langchain_core.messages import BaseMessage\n",
+ "from langchain_core.pydantic_v1 import BaseModel\n",
+ "\n",
+ "\n",
+ "class AgentState(BaseModel):\n",
+ " messages: Annotated[Sequence[BaseMessage], operator.add]"
+ ]
},
{
"cell_type": "markdown",
@@ -210,7 +260,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",
@@ -228,7 +324,50 @@
"id": "813ae66c-3b58-4283-a02a-36da72a2ab90",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"]
+ "source": [
+ "from langgraph.graph import END, StateGraph, START\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(AgentState)\n",
+ "\n",
+ "# Define the two nodes we will cycle between\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"action\", call_tool)\n",
+ "\n",
+ "# Set the entrypoint as `agent`\n",
+ "# This means that this node is the first one called\n",
+ "workflow.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\")\n",
+ "\n",
+ "# Finally, we compile it!\n",
+ "# This compiles it into a LangChain Runnable,\n",
+ "# meaning you can use it as you would any other runnable\n",
+ "app = workflow.compile()"
+ ]
},
{
"cell_type": "code",
@@ -247,7 +386,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",
@@ -289,7 +432,13 @@
]
}
],
- "source": ["from langchain_core.messages import HumanMessage\n\ninputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\nfor chunk in app.stream(inputs, stream_mode=\"values\"):\n chunk[\"messages\"][-1].pretty_print()"]
+ "source": [
+ "from langchain_core.messages import HumanMessage\n",
+ "\n",
+ "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n",
+ "for chunk in app.stream(inputs, stream_mode=\"values\"):\n",
+ " chunk[\"messages\"][-1].pretty_print()"
+ ]
},
{
"cell_type": "code",
@@ -297,7 +446,7 @@
"id": "296c7456-da05-4326-95dc-47d6b312da9d",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": []
}
],
"metadata": {
@@ -316,7 +465,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.2"
+ "version": "3.12.2"
}
},
"nbformat": 4,
diff --git a/examples/storm/storm.ipynb b/examples/storm/storm.ipynb
index 27a05804f..1138bf826 100644
--- a/examples/storm/storm.ipynb
+++ b/examples/storm/storm.ipynb
@@ -753,7 +753,7 @@
"execution_count": 73,
"metadata": {},
"outputs": [],
- "source": ["from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.set_finish_point(nodes[-1][0])\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())"]
+ "source": ["from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.add_edge(nodes[-1][0], END)\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())"]
},
{
"cell_type": "code",
diff --git a/examples/streaming-events-from-within-tools.ipynb b/examples/streaming-events-from-within-tools.ipynb
index 059c1186e..6cea6dc6e 100644
--- a/examples/streaming-events-from-within-tools.ipynb
+++ b/examples/streaming-events-from-within-tools.ipynb
@@ -116,7 +116,9 @@
"outputs": [],
"source": [
"@tool\n",
- "async def get_items(place: str, callbacks: Callbacks) -> str: # <--- Accept callbacks (Python <= 3.10)\n",
+ "async def get_items(\n",
+ " place: str, callbacks: Callbacks\n",
+ ") -> str: # <--- Accept callbacks (Python <= 3.10)\n",
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
" template = ChatPromptTemplate.from_messages(\n",
" [\n",
@@ -193,7 +195,9 @@
}
],
"source": [
- "async for event in agent.astream_events({\"messages\": [(\"human\", \"what items are on the shelf?\")]}, version=\"v2\"):\n",
+ "async for event in agent.astream_events(\n",
+ " {\"messages\": [(\"human\", \"what items are on the shelf?\")]}, version=\"v2\"\n",
+ "):\n",
" tags = event.get(\"tags\", [])\n",
" if event[\"event\"] == \"on_chat_model_stream\" and \"tool_llm\" in tags:\n",
" print(event[\"data\"][\"chunk\"].content, end=\"\", flush=True)"
diff --git a/examples/streaming-from-final-node.ipynb b/examples/streaming-from-final-node.ipynb
index bef24600a..ee7e14a4b 100644
--- a/examples/streaming-from-final-node.ipynb
+++ b/examples/streaming-from-final-node.ipynb
@@ -22,7 +22,9 @@
"id": "c04a3f8e-0bc9-430b-85db-3edfa026d2cd",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-openai"]
+ "source": [
+ "%%capture --no-stderr\n%pip install -U langgraph langchain-openai"
+ ]
},
{
"cell_type": "code",
@@ -38,7 +40,9 @@
]
}
],
- "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\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"
+ ]
},
{
"cell_type": "markdown",
@@ -54,7 +58,9 @@
"id": "1d51c35c-dbf2-4c01-932d-c5d308ea37d2",
"metadata": {},
"outputs": [],
- "source": ["from typing import Literal\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.runnables import ConfigurableField\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef get_weather(city: Literal[\"nyc\", \"sf\"]):\n \"\"\"Use this to get weather information.\"\"\"\n if city == \"nyc\":\n return \"It might be cloudy in nyc\"\n elif city == \"sf\":\n return \"It's always sunny in sf\"\n else:\n raise AssertionError(\"Unknown city\")\n\n\ntools = [get_weather]\nmodel = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\nfinal_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\nmodel = model.bind_tools(tools)\n# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\nfinal_model = final_model.with_config(tags=[\"final_node\"])"]
+ "source": [
+ "from typing import Literal\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.runnables import ConfigurableField\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef get_weather(city: Literal[\"nyc\", \"sf\"]):\n \"\"\"Use this to get weather information.\"\"\"\n if city == \"nyc\":\n return \"It might be cloudy in nyc\"\n elif city == \"sf\":\n return \"It's always sunny in sf\"\n else:\n raise AssertionError(\"Unknown city\")\n\n\ntools = [get_weather]\nmodel = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\nfinal_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\nmodel = model.bind_tools(tools)\n# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\nfinal_model = final_model.with_config(tags=[\"final_node\"])"
+ ]
},
{
"cell_type": "code",
@@ -62,7 +68,9 @@
"id": "0af37212-e592-484d-9194-35d53fa79678",
"metadata": {},
"outputs": [],
- "source": ["tool_node = ToolNode(tools=tools)"]
+ "source": [
+ "tool_node = ToolNode(tools=tools)"
+ ]
},
{
"cell_type": "code",
@@ -70,7 +78,9 @@
"id": "ac9d4f5b-655a-48f3-b514-a4a0815714a6",
"metadata": {},
"outputs": [],
- "source": ["from typing import TypedDict, Annotated\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import MessagesState\nfrom langchain_core.messages import BaseMessage"]
+ "source": [
+ "from typing import TypedDict, Annotated\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import MessagesState\nfrom langchain_core.messages import BaseMessage"
+ ]
},
{
"cell_type": "markdown",
@@ -86,7 +96,9 @@
"id": "3948c6b8-0317-4001-b699-32b25306a023",
"metadata": {},
"outputs": [],
- "source": ["from langchain_core.messages import SystemMessage, HumanMessage"]
+ "source": [
+ "from langchain_core.messages import SystemMessage, HumanMessage"
+ ]
},
{
"cell_type": "code",
@@ -94,7 +106,37 @@
"id": "2efe9fb4-c6c2-4171-becd-d45bbf899209",
"metadata": {},
"outputs": [],
- "source": ["def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n messages = state['messages']\n last_message = messages[-1]\n # If the LLM makes a tool call, then we route to the \"tools\" node\n if last_message.tool_calls:\n return \"tools\"\n # Otherwise, we stop (reply to the user)\n return \"final\"\n\n\ndef call_model(state: MessagesState):\n messages = state['messages']\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef call_final_model(state: MessagesState):\n messages = state['messages']\n last_ai_message = messages[-1]\n response = final_model.invoke([\n SystemMessage(\"Rewrite this in the voice of Al Roker\"),\n HumanMessage(last_ai_message.content)\n ])\n # overwrite the last AI message from the agent\n response.id = last_ai_message.id\n return {\"messages\": [response]}"]
+ "source": [
+ "def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ " # If the LLM makes a tool call, then we route to the \"tools\" node\n",
+ " if last_message.tool_calls:\n",
+ " return \"tools\"\n",
+ " # Otherwise, we stop (reply to the user)\n",
+ " return \"final\"\n",
+ "\n",
+ "\n",
+ "def call_model(state: MessagesState):\n",
+ " messages = state[\"messages\"]\n",
+ " response = model.invoke(messages)\n",
+ " # We return a list, because this will get added to the existing list\n",
+ " return {\"messages\": [response]}\n",
+ "\n",
+ "\n",
+ "def call_final_model(state: MessagesState):\n",
+ " messages = state[\"messages\"]\n",
+ " last_ai_message = messages[-1]\n",
+ " response = final_model.invoke(\n",
+ " [\n",
+ " SystemMessage(\"Rewrite this in the voice of Al Roker\"),\n",
+ " HumanMessage(last_ai_message.content),\n",
+ " ]\n",
+ " )\n",
+ " # overwrite the last AI message from the agent\n",
+ " response.id = last_ai_message.id\n",
+ " return {\"messages\": [response]}"
+ ]
},
{
"cell_type": "code",
@@ -102,7 +144,23 @@
"id": "b1a9a981-8629-4d25-a0e1-d666c3968b30",
"metadata": {},
"outputs": [],
- "source": ["workflow = StateGraph(MessagesState)\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"tools\", tool_node)\n# add a separate final node\nworkflow.add_node(\"final\", call_final_model)\n\nworkflow.add_edge(START, \"agent\")\nworkflow.add_conditional_edges(\n \"agent\",\n should_continue,\n)\n\nworkflow.add_edge(\"tools\", 'agent')\nworkflow.add_edge(\"final\", END)"]
+ "source": [
+ "workflow = StateGraph(MessagesState)\n",
+ "\n",
+ "workflow.add_node(\"agent\", call_model)\n",
+ "workflow.add_node(\"tools\", tool_node)\n",
+ "# add a separate final node\n",
+ "workflow.add_node(\"final\", call_final_model)\n",
+ "\n",
+ "workflow.add_edge(START, \"agent\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"agent\",\n",
+ " should_continue,\n",
+ ")\n",
+ "\n",
+ "workflow.add_edge(\"tools\", \"agent\")\n",
+ "workflow.add_edge(\"final\", END)"
+ ]
},
{
"cell_type": "code",
@@ -110,7 +168,9 @@
"id": "a7b0251f-dcee-49d6-8133-af50d4a55e22",
"metadata": {},
"outputs": [],
- "source": ["app = workflow.compile()"]
+ "source": [
+ "app = workflow.compile()"
+ ]
},
{
"cell_type": "code",
@@ -118,7 +178,9 @@
"id": "f8b77e74-17e9-4fee-a164-4637013b55ff",
"metadata": {},
"outputs": [],
- "source": ["from IPython.display import display, Image"]
+ "source": [
+ "from IPython.display import display, Image"
+ ]
},
{
"cell_type": "code",
@@ -137,7 +199,9 @@
"output_type": "display_data"
}
],
- "source": ["display(Image(app.get_graph().draw_mermaid_png()))"]
+ "source": [
+ "display(Image(app.get_graph().draw_mermaid_png()))"
+ ]
},
{
"cell_type": "markdown",
@@ -169,7 +233,9 @@
]
}
],
- "source": ["inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\nasync for event in app.astream_events(inputs, version=\"v2\"):\n kind = event[\"event\"]\n tags = event.get(\"tags\", [])\n if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n data = event[\"data\"]\n if data[\"chunk\"].content:\n # Empty content in the context of OpenAI or Anthropic usually means\n # that the model is asking for a tool to be invoked.\n # So we only print non-empty content\n print(data[\"chunk\"].content, end=\"|\")"]
+ "source": [
+ "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\nasync for event in app.astream_events(inputs, version=\"v2\"):\n kind = event[\"event\"]\n tags = event.get(\"tags\", [])\n if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n data = event[\"data\"]\n if data[\"chunk\"].content:\n # Empty content in the context of OpenAI or Anthropic usually means\n # that the model is asking for a tool to be invoked.\n # So we only print non-empty content\n print(data[\"chunk\"].content, end=\"|\")"
+ ]
}
],
"metadata": {
diff --git a/examples/subgraph.ipynb b/examples/subgraph.ipynb
index cbdd2cbf9..b56c143f5 100644
--- a/examples/subgraph.ipynb
+++ b/examples/subgraph.ipynb
@@ -22,7 +22,10 @@
"execution_count": 2,
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langgraph"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph"
+ ]
},
{
"cell_type": "markdown",
@@ -36,7 +39,19 @@
"execution_count": null,
"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\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_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",
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
+ "_set_env(\"LANGCHAIN_API_KEY\")"
+ ]
},
{
"cell_type": "markdown",
@@ -54,7 +69,60 @@
"execution_count": 1,
"metadata": {},
"outputs": [],
- "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n if not left:\n left = []\n if not right:\n right = []\n return left + right\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nchild_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.set_finish_point(\"child_end\")\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.set_finish_point(\"fin\")\ngraph = builder.compile()"]
+ "source": [
+ "from typing import Annotated\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START, END\n",
+ "\n",
+ "\n",
+ "def reduce_list(left: list | None, right: list | None) -> list:\n",
+ " if not left:\n",
+ " left = []\n",
+ " if not right:\n",
+ " right = []\n",
+ " return left + right\n",
+ "\n",
+ "\n",
+ "class ChildState(TypedDict):\n",
+ " name: str\n",
+ " path: Annotated[list[str], reduce_list]\n",
+ "\n",
+ "\n",
+ "class ParentState(TypedDict):\n",
+ " name: str\n",
+ " path: Annotated[list[str], reduce_list]\n",
+ "\n",
+ "\n",
+ "child_builder = StateGraph(ChildState)\n",
+ "\n",
+ "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
+ "child_builder.add_edge(START, \"child_start\")\n",
+ "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
+ "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n",
+ "child_builder.add_edge(\"child_start\", \"child_middle\")\n",
+ "child_builder.add_edge(\"child_middle\", \"child_end\")\n",
+ "child_builder.add_edge(\"child_end\", END)\n",
+ "\n",
+ "builder = StateGraph(ParentState)\n",
+ "\n",
+ "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
+ "builder.add_edge(START, \"grandparent\")\n",
+ "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
+ "builder.add_node(\"child\", child_builder.compile())\n",
+ "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
+ "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
+ "\n",
+ "# Add connections\n",
+ "builder.add_edge(\"grandparent\", \"parent\")\n",
+ "builder.add_edge(\"parent\", \"child\")\n",
+ "builder.add_edge(\"parent\", \"sibling\")\n",
+ "builder.add_edge(\"child\", \"fin\")\n",
+ "builder.add_edge(\"sibling\", \"fin\")\n",
+ "builder.add_edge(\"fin\", END)\n",
+ "graph = builder.compile()"
+ ]
},
{
"cell_type": "code",
@@ -72,7 +140,12 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "# Setting xray to 1 will show the internal structure of the nested graph\n",
+ "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -162,7 +235,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"name\": \"test\"}, debug=True)"]
+ "source": [
+ "graph.invoke({\"name\": \"test\"}, debug=True)"
+ ]
},
{
"cell_type": "markdown",
@@ -182,14 +257,79 @@
"execution_count": 23,
"metadata": {},
"outputs": [],
- "source": ["import uuid\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n if not left:\n left = []\n if not right:\n right = []\n left_, right_ = [], []\n for orig, new in [(left, left_), (right, right_)]:\n for val in orig:\n if not isinstance(val, dict):\n val = {\"val\": val}\n if \"id\" not in val:\n val[\"id\"] = str(uuid.uuid4())\n new.append(val)\n # Merge the two lists\n left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n merged = left_.copy()\n for val in right_:\n if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n merged[existing_idx] = val\n else:\n merged.append(val)\n return merged\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]"]
+ "source": [
+ "import uuid\n",
+ "\n",
+ "\n",
+ "def reduce_list(left: list | None, right: list | None) -> list:\n",
+ " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n",
+ " if not left:\n",
+ " left = []\n",
+ " if not right:\n",
+ " right = []\n",
+ " left_, right_ = [], []\n",
+ " for orig, new in [(left, left_), (right, right_)]:\n",
+ " for val in orig:\n",
+ " if not isinstance(val, dict):\n",
+ " val = {\"val\": val}\n",
+ " if \"id\" not in val:\n",
+ " val[\"id\"] = str(uuid.uuid4())\n",
+ " new.append(val)\n",
+ " # Merge the two lists\n",
+ " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n",
+ " merged = left_.copy()\n",
+ " for val in right_:\n",
+ " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n",
+ " merged[existing_idx] = val\n",
+ " else:\n",
+ " merged.append(val)\n",
+ " return merged\n",
+ "\n",
+ "\n",
+ "class ChildState(TypedDict):\n",
+ " name: str\n",
+ " path: Annotated[list[str], reduce_list]\n",
+ "\n",
+ "\n",
+ "class ParentState(TypedDict):\n",
+ " name: str\n",
+ " path: Annotated[list[str], reduce_list]"
+ ]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
- "source": ["child_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.set_finish_point(\"child_end\")\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.set_finish_point(\"fin\")\ngraph = builder.compile()"]
+ "source": [
+ "child_builder = StateGraph(ChildState)\n",
+ "\n",
+ "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
+ "child_builder.add_edge(START, \"child_start\")\n",
+ "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
+ "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n",
+ "child_builder.add_edge(\"child_start\", \"child_middle\")\n",
+ "child_builder.add_edge(\"child_middle\", \"child_end\")\n",
+ "child_builder.add_edge(\"child_end\", END)\n",
+ "\n",
+ "builder = StateGraph(ParentState)\n",
+ "\n",
+ "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
+ "builder.add_edge(START, \"grandparent\")\n",
+ "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
+ "builder.add_node(\"child\", child_builder.compile())\n",
+ "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
+ "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
+ "\n",
+ "# Add connections\n",
+ "builder.add_edge(\"grandparent\", \"parent\")\n",
+ "builder.add_edge(\"parent\", \"child\")\n",
+ "builder.add_edge(\"parent\", \"sibling\")\n",
+ "builder.add_edge(\"child\", \"fin\")\n",
+ "builder.add_edge(\"sibling\", \"fin\")\n",
+ "builder.add_edge(\"fin\", END)\n",
+ "graph = builder.compile()"
+ ]
},
{
"cell_type": "code",
@@ -207,7 +347,12 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "\n",
+ "# Setting xray to 1 will show the internal structure of the nested graph\n",
+ "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))"
+ ]
},
{
"cell_type": "code",
@@ -301,7 +446,9 @@
"output_type": "execute_result"
}
],
- "source": ["graph.invoke({\"name\": \"test\"}, debug=True)"]
+ "source": [
+ "graph.invoke({\"name\": \"test\"}, debug=True)"
+ ]
}
],
"metadata": {
@@ -320,7 +467,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.2"
+ "version": "3.12.2"
}
},
"nbformat": 4,
diff --git a/examples/tutorials/rag-agent-testing.ipynb b/examples/tutorials/rag-agent-testing.ipynb
index bd0b67dca..bfbd5fd5a 100644
--- a/examples/tutorials/rag-agent-testing.ipynb
+++ b/examples/tutorials/rag-agent-testing.ipynb
@@ -129,6 +129,7 @@
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
+ "\n",
"model_tested = \"gpt-4o\"\n",
"metadata = \"CRAG, gpt-4o\"\n",
"llm = ChatOpenAI(model_name=model_tested, temperature=0)"
@@ -150,6 +151,7 @@
"outputs": [],
"source": [
"from langchain_fireworks import ChatFireworks\n",
+ "\n",
"model_tested = \"firefunction-v2\"\n",
"metadata = \"CRAG, firefunction-v\"\n",
"llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v2\", temperature=0)"
@@ -344,9 +346,9 @@
"source": [
"@tool\n",
"def generate_answer(answer: str) -> str:\n",
- " \"\"\"You are an assistant for question-answering tasks. \n",
- " Use the retrieved documents to answer the user question. \n",
- " If you don't know the answer, just say that you don't know. \n",
+ " \"\"\"You are an assistant for question-answering tasks.\n",
+ " Use the retrieved documents to answer the user question.\n",
+ " If you don't know the answer, just say that you don't know.\n",
" Use three sentences maximum and keep the answer concise\"\"\"\n",
" return f\"Here is the answer to the user question: {answer}\""
]
@@ -439,7 +441,7 @@
" (\n",
" \"system\",\n",
" \" You are a helpful assistant tasked with answering user questions using the provided vector store. \"\n",
- " \" Use the provided vector store to retrieve documents. Then grade them to ensure they are relevant before answering the question. \"\n",
+ " \" Use the provided vector store to retrieve documents. Then grade them to ensure they are relevant before answering the question. \",\n",
" ),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
@@ -629,7 +631,8 @@
" ]\n",
" return tool_calls\n",
"\n",
- "find_tool_calls_react(response['messages'])"
+ "\n",
+ "find_tool_calls_react(response[\"messages\"])"
]
},
{
@@ -1080,6 +1083,7 @@
"# Grade prompt\n",
"grade_prompt_answer_accuracy = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n",
"\n",
+ "\n",
"def answer_evaluator(run, example) -> dict:\n",
" \"\"\"\n",
" A simple evaluator for RAG answer accuracy\n",
@@ -1140,6 +1144,7 @@
" \"generate_answer\",\n",
"]\n",
"\n",
+ "\n",
"def check_trajectory_react(root_run: Run, example: Example) -> dict:\n",
" \"\"\"\n",
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
diff --git a/examples/tutorials/sql-agent.ipynb b/examples/tutorials/sql-agent.ipynb
index 4d07901b4..fb1354ef4 100644
--- a/examples/tutorials/sql-agent.ipynb
+++ b/examples/tutorials/sql-agent.ipynb
@@ -59,7 +59,13 @@
}
},
"outputs": [],
- "source": ["import os\n\nos.environ[\"OPENAI_API_KEY\"] = \"sk-...\"\nos.environ[\"LANGSMITH_API_KEY\"] = \"lsv2_pt_...\"\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""]
+ "source": [
+ "import os\n",
+ "\n",
+ "os.environ[\"OPENAI_API_KEY\"] = \"sk-...\"\n",
+ "os.environ[\"LANGSMITH_API_KEY\"] = \"lsv2_pt_...\"\n",
+ "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""
+ ]
},
{
"cell_type": "code",
@@ -67,7 +73,9 @@
"id": "04d73c39-1cc9-4b94-a454-b0a4f604713c",
"metadata": {},
"outputs": [],
- "source": ["os.environ[\"LANGCHAIN_PROJECT\"] = \"sql-agent\""]
+ "source": [
+ "os.environ[\"LANGCHAIN_PROJECT\"] = \"sql-agent\""
+ ]
},
{
"cell_type": "markdown",
@@ -110,7 +118,22 @@
]
}
],
- "source": ["import requests\n\nurl = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n\nresponse = requests.get(url)\n\nif response.status_code == 200:\n # Open a local file in binary write mode\n with open(\"Chinook.db\", \"wb\") as file:\n # Write the content of the response (the file) to the local file\n file.write(response.content)\n print(\"File downloaded and saved as Chinook.db\")\nelse:\n print(f\"Failed to download the file. Status code: {response.status_code}\")"]
+ "source": [
+ "import requests\n",
+ "\n",
+ "url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n",
+ "\n",
+ "response = requests.get(url)\n",
+ "\n",
+ "if response.status_code == 200:\n",
+ " # Open a local file in binary write mode\n",
+ " with open(\"Chinook.db\", \"wb\") as file:\n",
+ " # Write the content of the response (the file) to the local file\n",
+ " file.write(response.content)\n",
+ " print(\"File downloaded and saved as Chinook.db\")\n",
+ "else:\n",
+ " print(f\"Failed to download the file. Status code: {response.status_code}\")"
+ ]
},
{
"cell_type": "markdown",
@@ -140,7 +163,10 @@
}
},
"outputs": [],
- "source": ["%%capture --no-stderr --no-display\n!pip install langgraph langchain_community langchain_openai"]
+ "source": [
+ "%%capture --no-stderr --no-display\n",
+ "!pip install langgraph langchain_community langchain_openai"
+ ]
},
{
"cell_type": "code",
@@ -176,7 +202,14 @@
"output_type": "execute_result"
}
],
- "source": ["from langchain_community.utilities import SQLDatabase\n\ndb = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\nprint(db.dialect)\nprint(db.get_usable_table_names())\ndb.run(\"SELECT * FROM Artist LIMIT 10;\")"]
+ "source": [
+ "from langchain_community.utilities import SQLDatabase\n",
+ "\n",
+ "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n",
+ "print(db.dialect)\n",
+ "print(db.get_usable_table_names())\n",
+ "db.run(\"SELECT * FROM Artist LIMIT 10;\")"
+ ]
},
{
"cell_type": "markdown",
@@ -208,7 +241,36 @@
}
},
"outputs": [],
- "source": ["from typing import Any\n\nfrom langchain_core.messages import ToolMessage\nfrom langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\nfrom langgraph.prebuilt import ToolNode\n\n\ndef create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n \"\"\"\n Create a ToolNode with a fallback to handle errors and surface them to the agent.\n \"\"\"\n return ToolNode(tools).with_fallbacks(\n [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n )\n\n\ndef handle_tool_error(state) -> dict:\n error = state.get(\"error\")\n tool_calls = state[\"messages\"][-1].tool_calls\n return {\n \"messages\": [\n ToolMessage(\n content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n tool_call_id=tc[\"id\"],\n )\n for tc in tool_calls\n ]\n }"]
+ "source": [
+ "from typing import Any\n",
+ "\n",
+ "from langchain_core.messages import ToolMessage\n",
+ "from langchain_core.runnables import RunnableLambda, RunnableWithFallbacks\n",
+ "from langgraph.prebuilt import ToolNode\n",
+ "\n",
+ "\n",
+ "def create_tool_node_with_fallback(tools: list) -> RunnableWithFallbacks[Any, dict]:\n",
+ " \"\"\"\n",
+ " Create a ToolNode with a fallback to handle errors and surface them to the agent.\n",
+ " \"\"\"\n",
+ " return ToolNode(tools).with_fallbacks(\n",
+ " [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n",
+ " )\n",
+ "\n",
+ "\n",
+ "def handle_tool_error(state) -> dict:\n",
+ " error = state.get(\"error\")\n",
+ " tool_calls = state[\"messages\"][-1].tool_calls\n",
+ " return {\n",
+ " \"messages\": [\n",
+ " ToolMessage(\n",
+ " content=f\"Error: {repr(error)}\\n please fix your mistakes.\",\n",
+ " tool_call_id=tc[\"id\"],\n",
+ " )\n",
+ " for tc in tool_calls\n",
+ " ]\n",
+ " }"
+ ]
},
{
"cell_type": "markdown",
@@ -268,7 +330,20 @@
]
}
],
- "source": ["from langchain_community.agent_toolkits import SQLDatabaseToolkit\nfrom langchain_openai import ChatOpenAI\n\ntoolkit = SQLDatabaseToolkit(db=db, llm=ChatOpenAI(model=\"gpt-4o\"))\ntools = toolkit.get_tools()\n\nlist_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\nget_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n\nprint(list_tables_tool.invoke(\"\"))\n\nprint(get_schema_tool.invoke(\"Artist\"))"]
+ "source": [
+ "from langchain_community.agent_toolkits import SQLDatabaseToolkit\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "toolkit = SQLDatabaseToolkit(db=db, llm=ChatOpenAI(model=\"gpt-4o\"))\n",
+ "tools = toolkit.get_tools()\n",
+ "\n",
+ "list_tables_tool = next(tool for tool in tools if tool.name == \"sql_db_list_tables\")\n",
+ "get_schema_tool = next(tool for tool in tools if tool.name == \"sql_db_schema\")\n",
+ "\n",
+ "print(list_tables_tool.invoke(\"\"))\n",
+ "\n",
+ "print(get_schema_tool.invoke(\"Artist\"))"
+ ]
},
{
"cell_type": "markdown",
@@ -306,7 +381,25 @@
]
}
],
- "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef db_query_tool(query: str) -> str:\n \"\"\"\n Execute a SQL query against the database and get back the result.\n If the query is not correct, an error message will be returned.\n If an error is returned, rewrite the query, check the query, and try again.\n \"\"\"\n result = db.run_no_throw(query)\n if not result:\n return \"Error: Query failed. Please rewrite your query and try again.\"\n return result\n\n\nprint(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))"]
+ "source": [
+ "from langchain_core.tools import tool\n",
+ "\n",
+ "\n",
+ "@tool\n",
+ "def db_query_tool(query: str) -> str:\n",
+ " \"\"\"\n",
+ " Execute a SQL query against the database and get back the result.\n",
+ " If the query is not correct, an error message will be returned.\n",
+ " If an error is returned, rewrite the query, check the query, and try again.\n",
+ " \"\"\"\n",
+ " result = db.run_no_throw(query)\n",
+ " if not result:\n",
+ " return \"Error: Query failed. Please rewrite your query and try again.\"\n",
+ " return result\n",
+ "\n",
+ "\n",
+ "print(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))"
+ ]
},
{
"cell_type": "markdown",
@@ -347,7 +440,33 @@
"output_type": "execute_result"
}
],
- "source": ["from langchain_core.prompts import ChatPromptTemplate\n\nquery_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\nDouble check the SQLite query for common mistakes, including:\n- Using NOT IN with NULL values\n- Using UNION when UNION ALL should have been used\n- Using BETWEEN for exclusive ranges\n- Data type mismatch in predicates\n- Properly quoting identifiers\n- Using the correct number of arguments for functions\n- Casting to the correct data type\n- Using the proper columns for joins\n\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n\nYou will call the appropriate tool to execute the query after running this check.\"\"\"\n\nquery_check_prompt = ChatPromptTemplate.from_messages(\n [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n)\nquery_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n [db_query_tool], tool_choice=\"required\"\n)\n\nquery_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})"]
+ "source": [
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "\n",
+ "query_check_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
+ "Double check the SQLite query for common mistakes, including:\n",
+ "- Using NOT IN with NULL values\n",
+ "- Using UNION when UNION ALL should have been used\n",
+ "- Using BETWEEN for exclusive ranges\n",
+ "- Data type mismatch in predicates\n",
+ "- Properly quoting identifiers\n",
+ "- Using the correct number of arguments for functions\n",
+ "- Casting to the correct data type\n",
+ "- Using the proper columns for joins\n",
+ "\n",
+ "If there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n",
+ "\n",
+ "You will call the appropriate tool to execute the query after running this check.\"\"\"\n",
+ "\n",
+ "query_check_prompt = ChatPromptTemplate.from_messages(\n",
+ " [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n",
+ ")\n",
+ "query_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
+ " [db_query_tool], tool_choice=\"required\"\n",
+ ")\n",
+ "\n",
+ "query_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})"
+ ]
},
{
"cell_type": "markdown",
@@ -379,7 +498,167 @@
}
},
"outputs": [],
- "source": ["from typing import Annotated, Literal\n\nfrom langchain_core.messages import AIMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\n# Define the state for the agent\nclass State(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Add a node for the first tool call\ndef first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n return {\n \"messages\": [\n AIMessage(\n content=\"\",\n tool_calls=[\n {\n \"name\": \"sql_db_list_tables\",\n \"args\": {},\n \"id\": \"tool_abcd123\",\n }\n ],\n )\n ]\n }\n\n\ndef model_check_query(state: State) -> dict[str, list[AIMessage]]:\n \"\"\"\n Use this tool to double-check if your query is correct before executing it.\n \"\"\"\n return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n\n\nworkflow.add_node(\"first_tool_call\", first_tool_call)\n\n# Add nodes for the first two tools\nworkflow.add_node(\n \"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool])\n)\nworkflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n\n# Add a node for a model to choose the relevant tables based on the question and available tables\nmodel_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n [get_schema_tool]\n)\nworkflow.add_node(\n \"model_get_schema\",\n lambda state: {\n \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n },\n)\n\n\n# Describe a tool to represent the end state\nclass SubmitFinalAnswer(BaseModel):\n \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n\n final_answer: str = Field(..., description=\"The final answer to the user\")\n\n\n# Add a node for a model to generate a query based on the question and schema\nquery_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n\nGiven an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n\nDO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n\nWhen generating the query:\n\nOutput the SQL query that answers the input question without a tool call.\n\nUnless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\nYou can order the results by a relevant column to return the most interesting examples in the database.\nNever query for all the columns from a specific table, only ask for the relevant columns given the question.\n\nIf you get an error while executing a query, rewrite the query and try again.\n\nIf you get an empty result set, you should try to rewrite the query to get a non-empty result set. \nNEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n\nIf you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n\nDO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\nquery_gen_prompt = ChatPromptTemplate.from_messages(\n [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n)\nquery_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n [SubmitFinalAnswer]\n)\n\n\ndef query_gen_node(state: State):\n message = query_gen.invoke(state)\n\n # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n tool_messages = []\n if message.tool_calls:\n for tc in message.tool_calls:\n if tc[\"name\"] != \"SubmitFinalAnswer\":\n tool_messages.append(\n ToolMessage(\n content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",\n tool_call_id=tc[\"id\"],\n )\n )\n else:\n tool_messages = []\n return {\"messages\": [message] + tool_messages}\n\n\nworkflow.add_node(\"query_gen\", query_gen_node)\n\n# Add a node for the model to check the query before executing it\nworkflow.add_node(\"correct_query\", model_check_query)\n\n# Add node for executing the query\nworkflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))\n\n\n# Define a conditional edge to decide whether to continue or end the workflow\ndef should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is a tool call, then we finish\n if getattr(last_message, \"tool_calls\", None):\n return END\n if last_message.content.startswith(\"Error:\"):\n return \"query_gen\"\n else:\n return \"correct_query\"\n\n\n# Specify the edges between the nodes\nworkflow.add_edge(START, \"first_tool_call\")\nworkflow.add_edge(\"first_tool_call\", \"list_tables_tool\")\nworkflow.add_edge(\"list_tables_tool\", \"model_get_schema\")\nworkflow.add_edge(\"model_get_schema\", \"get_schema_tool\")\nworkflow.add_edge(\"get_schema_tool\", \"query_gen\")\nworkflow.add_conditional_edges(\n \"query_gen\",\n should_continue,\n)\nworkflow.add_edge(\"correct_query\", \"execute_query\")\nworkflow.add_edge(\"execute_query\", \"query_gen\")\n\n# Compile the workflow into a runnable\napp = workflow.compile()"]
+ "source": [
+ "from typing import Annotated, Literal\n",
+ "\n",
+ "from langchain_core.messages import AIMessage\n",
+ "from langchain_core.pydantic_v1 import BaseModel, Field\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import END, StateGraph, START\n",
+ "from langgraph.graph.message import AnyMessage, add_messages\n",
+ "\n",
+ "\n",
+ "# Define the state for the agent\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list[AnyMessage], add_messages]\n",
+ "\n",
+ "\n",
+ "# Define a new graph\n",
+ "workflow = StateGraph(State)\n",
+ "\n",
+ "\n",
+ "# Add a node for the first tool call\n",
+ "def first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n",
+ " return {\n",
+ " \"messages\": [\n",
+ " AIMessage(\n",
+ " content=\"\",\n",
+ " tool_calls=[\n",
+ " {\n",
+ " \"name\": \"sql_db_list_tables\",\n",
+ " \"args\": {},\n",
+ " \"id\": \"tool_abcd123\",\n",
+ " }\n",
+ " ],\n",
+ " )\n",
+ " ]\n",
+ " }\n",
+ "\n",
+ "\n",
+ "def model_check_query(state: State) -> dict[str, list[AIMessage]]:\n",
+ " \"\"\"\n",
+ " Use this tool to double-check if your query is correct before executing it.\n",
+ " \"\"\"\n",
+ " return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n",
+ "\n",
+ "\n",
+ "workflow.add_node(\"first_tool_call\", first_tool_call)\n",
+ "\n",
+ "# Add nodes for the first two tools\n",
+ "workflow.add_node(\n",
+ " \"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool])\n",
+ ")\n",
+ "workflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n",
+ "\n",
+ "# Add a node for a model to choose the relevant tables based on the question and available tables\n",
+ "model_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
+ " [get_schema_tool]\n",
+ ")\n",
+ "workflow.add_node(\n",
+ " \"model_get_schema\",\n",
+ " lambda state: {\n",
+ " \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n",
+ " },\n",
+ ")\n",
+ "\n",
+ "\n",
+ "# Describe a tool to represent the end state\n",
+ "class SubmitFinalAnswer(BaseModel):\n",
+ " \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n",
+ "\n",
+ " final_answer: str = Field(..., description=\"The final answer to the user\")\n",
+ "\n",
+ "\n",
+ "# Add a node for a model to generate a query based on the question and schema\n",
+ "query_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
+ "\n",
+ "Given an input question, output a syntactically correct SQLite query to run, then look at the results of the query and return the answer.\n",
+ "\n",
+ "DO NOT call any tool besides SubmitFinalAnswer to submit the final answer.\n",
+ "\n",
+ "When generating the query:\n",
+ "\n",
+ "Output the SQL query that answers the input question without a tool call.\n",
+ "\n",
+ "Unless the user specifies a specific number of examples they wish to obtain, always limit your query to at most 5 results.\n",
+ "You can order the results by a relevant column to return the most interesting examples in the database.\n",
+ "Never query for all the columns from a specific table, only ask for the relevant columns given the question.\n",
+ "\n",
+ "If you get an error while executing a query, rewrite the query and try again.\n",
+ "\n",
+ "If you get an empty result set, you should try to rewrite the query to get a non-empty result set. \n",
+ "NEVER make stuff up if you don't have enough information to answer the query... just say you don't have enough information.\n",
+ "\n",
+ "If you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n",
+ "\n",
+ "DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\n",
+ "query_gen_prompt = ChatPromptTemplate.from_messages(\n",
+ " [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n",
+ ")\n",
+ "query_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
+ " [SubmitFinalAnswer]\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def query_gen_node(state: State):\n",
+ " message = query_gen.invoke(state)\n",
+ "\n",
+ " # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n",
+ " tool_messages = []\n",
+ " if message.tool_calls:\n",
+ " for tc in message.tool_calls:\n",
+ " if tc[\"name\"] != \"SubmitFinalAnswer\":\n",
+ " tool_messages.append(\n",
+ " ToolMessage(\n",
+ " content=f\"Error: The wrong tool was called: {tc['name']}. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.\",\n",
+ " tool_call_id=tc[\"id\"],\n",
+ " )\n",
+ " )\n",
+ " else:\n",
+ " tool_messages = []\n",
+ " return {\"messages\": [message] + tool_messages}\n",
+ "\n",
+ "\n",
+ "workflow.add_node(\"query_gen\", query_gen_node)\n",
+ "\n",
+ "# Add a node for the model to check the query before executing it\n",
+ "workflow.add_node(\"correct_query\", model_check_query)\n",
+ "\n",
+ "# Add node for executing the query\n",
+ "workflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))\n",
+ "\n",
+ "\n",
+ "# Define a conditional edge to decide whether to continue or end the workflow\n",
+ "def should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n",
+ " messages = state[\"messages\"]\n",
+ " last_message = messages[-1]\n",
+ " # If there is a tool call, then we finish\n",
+ " if getattr(last_message, \"tool_calls\", None):\n",
+ " return END\n",
+ " if last_message.content.startswith(\"Error:\"):\n",
+ " return \"query_gen\"\n",
+ " else:\n",
+ " return \"correct_query\"\n",
+ "\n",
+ "\n",
+ "# Specify the edges between the nodes\n",
+ "workflow.add_edge(START, \"first_tool_call\")\n",
+ "workflow.add_edge(\"first_tool_call\", \"list_tables_tool\")\n",
+ "workflow.add_edge(\"list_tables_tool\", \"model_get_schema\")\n",
+ "workflow.add_edge(\"model_get_schema\", \"get_schema_tool\")\n",
+ "workflow.add_edge(\"get_schema_tool\", \"query_gen\")\n",
+ "workflow.add_conditional_edges(\n",
+ " \"query_gen\",\n",
+ " should_continue,\n",
+ ")\n",
+ "workflow.add_edge(\"correct_query\", \"execute_query\")\n",
+ "workflow.add_edge(\"execute_query\", \"query_gen\")\n",
+ "\n",
+ "# Compile the workflow into a runnable\n",
+ "app = workflow.compile()"
+ ]
},
{
"cell_type": "markdown",
@@ -420,7 +699,18 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\nfrom langchain_core.runnables.graph import MermaidDrawMethod\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n draw_method=MermaidDrawMethod.API,\n )\n )\n)"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "from langchain_core.runnables.graph import MermaidDrawMethod\n",
+ "\n",
+ "display(\n",
+ " Image(\n",
+ " app.get_graph().draw_mermaid_png(\n",
+ " draw_method=MermaidDrawMethod.API,\n",
+ " )\n",
+ " )\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -452,7 +742,17 @@
"output_type": "execute_result"
}
],
- "source": ["import json\n\nmessages = app.invoke(\n {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n)\njson_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n \"arguments\"\n]\njson.loads(json_str)[\"final_answer\"]"]
+ "source": [
+ "import json\n",
+ "\n",
+ "messages = app.invoke(\n",
+ " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n",
+ ")\n",
+ "json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n",
+ " \"arguments\"\n",
+ "]\n",
+ "json.loads(json_str)[\"final_answer\"]"
+ ]
},
{
"cell_type": "code",
@@ -460,7 +760,12 @@
"id": "3bf7709f-500c-4f28-bb85-dda317286c63",
"metadata": {},
"outputs": [],
- "source": ["for event in app.stream(\n {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n):\n print(event)"]
+ "source": [
+ "for event in app.stream(\n",
+ " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n",
+ "):\n",
+ " print(event)"
+ ]
},
{
"attachments": {
@@ -493,7 +798,20 @@
"id": "a80f4adc-a8dc-403c-9bef-6de5e873b9bc",
"metadata": {},
"outputs": [],
- "source": ["import json\n\n\ndef predict_sql_agent_answer(example: dict):\n \"\"\"Use this for answer evaluation\"\"\"\n msg = {\"messages\": (\"user\", example[\"input\"])}\n messages = app.invoke(msg)\n json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n \"arguments\"\n ]\n response = json.loads(json_str)[\"final_answer\"]\n return {\"response\": response}"]
+ "source": [
+ "import json\n",
+ "\n",
+ "\n",
+ "def predict_sql_agent_answer(example: dict):\n",
+ " \"\"\"Use this for answer evaluation\"\"\"\n",
+ " msg = {\"messages\": (\"user\", example[\"input\"])}\n",
+ " messages = app.invoke(msg)\n",
+ " json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n",
+ " \"arguments\"\n",
+ " ]\n",
+ " response = json.loads(json_str)[\"final_answer\"]\n",
+ " return {\"response\": response}"
+ ]
},
{
"cell_type": "code",
@@ -501,7 +819,42 @@
"id": "1040233f-3751-4bd3-902f-709fc2e1ecf5",
"metadata": {},
"outputs": [],
- "source": ["from langchain import hub\nfrom langchain_openai import ChatOpenAI\n\n# Grade prompt\ngrade_prompt_answer_accuracy = prompt = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n\n\ndef answer_evaluator(run, example) -> dict:\n \"\"\"\n A simple evaluator for RAG answer accuracy\n \"\"\"\n\n # Get question, ground truth answer, chain\n input_question = example.inputs[\"input\"]\n reference = example.outputs[\"output\"]\n prediction = run.outputs[\"response\"]\n\n # LLM grader\n llm = ChatOpenAI(model=\"gpt-4-turbo\", temperature=0)\n\n # Structured prompt\n answer_grader = grade_prompt_answer_accuracy | llm\n\n # Run evaluator\n score = answer_grader.invoke(\n {\n \"question\": input_question,\n \"correct_answer\": reference,\n \"student_answer\": prediction,\n }\n )\n score = score[\"Score\"]\n\n return {\"key\": \"answer_v_reference_score\", \"score\": score}"]
+ "source": [
+ "from langchain import hub\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "# Grade prompt\n",
+ "grade_prompt_answer_accuracy = prompt = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n",
+ "\n",
+ "\n",
+ "def answer_evaluator(run, example) -> dict:\n",
+ " \"\"\"\n",
+ " A simple evaluator for RAG answer accuracy\n",
+ " \"\"\"\n",
+ "\n",
+ " # Get question, ground truth answer, chain\n",
+ " input_question = example.inputs[\"input\"]\n",
+ " reference = example.outputs[\"output\"]\n",
+ " prediction = run.outputs[\"response\"]\n",
+ "\n",
+ " # LLM grader\n",
+ " llm = ChatOpenAI(model=\"gpt-4-turbo\", temperature=0)\n",
+ "\n",
+ " # Structured prompt\n",
+ " answer_grader = grade_prompt_answer_accuracy | llm\n",
+ "\n",
+ " # Run evaluator\n",
+ " score = answer_grader.invoke(\n",
+ " {\n",
+ " \"question\": input_question,\n",
+ " \"correct_answer\": reference,\n",
+ " \"student_answer\": prediction,\n",
+ " }\n",
+ " )\n",
+ " score = score[\"Score\"]\n",
+ "\n",
+ " return {\"key\": \"answer_v_reference_score\", \"score\": score}"
+ ]
},
{
"cell_type": "code",
@@ -509,7 +862,19 @@
"id": "eb814b85-70ba-4699-9038-20266b53efbd",
"metadata": {},
"outputs": [],
- "source": ["from langsmith.evaluation import evaluate\n\ndataset_name = \"SQL Agent Response\"\nexperiment_results = evaluate(\n predict_sql_agent_answer,\n data=dataset_name,\n evaluators=[answer_evaluator],\n num_repetitions=3,\n experiment_prefix=\"sql-agent-multi-step-response-v-reference\",\n metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n)"]
+ "source": [
+ "from langsmith.evaluation import evaluate\n",
+ "\n",
+ "dataset_name = \"SQL Agent Response\"\n",
+ "experiment_results = evaluate(\n",
+ " predict_sql_agent_answer,\n",
+ " data=dataset_name,\n",
+ " evaluators=[answer_evaluator],\n",
+ " num_repetitions=3,\n",
+ " experiment_prefix=\"sql-agent-multi-step-response-v-reference\",\n",
+ " metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n",
+ ")"
+ ]
},
{
"attachments": {
@@ -544,7 +909,15 @@
"id": "ef84d2f7-fa52-46ca-8939-616e5ac4d101",
"metadata": {},
"outputs": [],
- "source": ["# These are the tools that we expect the agent to use\nexpected_trajectory = [\n \"sql_db_list_tables\", # first: list_tables_tool node\n \"sql_db_schema\", # second: get_schema_tool node\n \"db_query_tool\", # third: execute_query node\n \"SubmitFinalAnswer\",\n] # fourth: query_gen"]
+ "source": [
+ "# These are the tools that we expect the agent to use\n",
+ "expected_trajectory = [\n",
+ " \"sql_db_list_tables\", # first: list_tables_tool node\n",
+ " \"sql_db_schema\", # second: get_schema_tool node\n",
+ " \"db_query_tool\", # third: execute_query node\n",
+ " \"SubmitFinalAnswer\",\n",
+ "] # fourth: query_gen"
+ ]
},
{
"cell_type": "code",
@@ -552,7 +925,13 @@
"id": "1b7b007a-1dd2-4f3e-b157-b9d7ec2ea0a2",
"metadata": {},
"outputs": [],
- "source": ["def predict_sql_agent_messages(example: dict):\n \"\"\"Use this for answer evaluation\"\"\"\n msg = {\"messages\": (\"user\", example[\"input\"])}\n messages = app.invoke(msg)\n return {\"response\": messages}"]
+ "source": [
+ "def predict_sql_agent_messages(example: dict):\n",
+ " \"\"\"Use this for answer evaluation\"\"\"\n",
+ " msg = {\"messages\": (\"user\", example[\"input\"])}\n",
+ " messages = app.invoke(msg)\n",
+ " return {\"response\": messages}"
+ ]
},
{
"cell_type": "code",
@@ -560,7 +939,67 @@
"id": "ae2fe538-1c6d-4186-80dd-1d240d253f40",
"metadata": {},
"outputs": [],
- "source": ["from langsmith.schemas import Example, Run\n\n\ndef find_tool_calls(messages):\n \"\"\"\n Find all tool calls in the messages returned\n \"\"\"\n tool_calls = [\n tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n ]\n return tool_calls\n\n\ndef contains_all_tool_calls_in_order_exact_match(\n root_run: Run, example: Example\n) -> dict:\n \"\"\"\n Check if all expected tools are called in exact order and without any additional tool calls.\n \"\"\"\n expected_trajectory = [\n \"sql_db_list_tables\",\n \"sql_db_schema\",\n \"db_query_tool\",\n \"SubmitFinalAnswer\",\n ]\n messages = root_run.outputs[\"response\"]\n tool_calls = find_tool_calls(messages)\n\n # Print the tool calls for debugging\n print(\"Here are my tool calls:\")\n print(tool_calls)\n\n # Check if the tool calls match the expected trajectory exactly\n if tool_calls == expected_trajectory:\n score = 1\n else:\n score = 0\n\n return {\"score\": int(score), \"key\": \"multi_tool_call_in_exact_order\"}\n\n\ndef contains_all_tool_calls_in_order(root_run: Run, example: Example) -> dict:\n \"\"\"\n Check if all expected tools are called in order,\n but it allows for other tools to be called in between the expected ones.\n \"\"\"\n messages = root_run.outputs[\"response\"]\n tool_calls = find_tool_calls(messages)\n\n # Print the tool calls for debugging\n print(\"Here are my tool calls:\")\n print(tool_calls)\n\n it = iter(tool_calls)\n if all(elem in it for elem in expected_trajectory):\n score = 1\n else:\n score = 0\n return {\"score\": int(score), \"key\": \"multi_tool_call_in_order\"}"]
+ "source": [
+ "from langsmith.schemas import Example, Run\n",
+ "\n",
+ "\n",
+ "def find_tool_calls(messages):\n",
+ " \"\"\"\n",
+ " Find all tool calls in the messages returned\n",
+ " \"\"\"\n",
+ " tool_calls = [\n",
+ " tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
+ " ]\n",
+ " return tool_calls\n",
+ "\n",
+ "\n",
+ "def contains_all_tool_calls_in_order_exact_match(\n",
+ " root_run: Run, example: Example\n",
+ ") -> dict:\n",
+ " \"\"\"\n",
+ " Check if all expected tools are called in exact order and without any additional tool calls.\n",
+ " \"\"\"\n",
+ " expected_trajectory = [\n",
+ " \"sql_db_list_tables\",\n",
+ " \"sql_db_schema\",\n",
+ " \"db_query_tool\",\n",
+ " \"SubmitFinalAnswer\",\n",
+ " ]\n",
+ " messages = root_run.outputs[\"response\"]\n",
+ " tool_calls = find_tool_calls(messages)\n",
+ "\n",
+ " # Print the tool calls for debugging\n",
+ " print(\"Here are my tool calls:\")\n",
+ " print(tool_calls)\n",
+ "\n",
+ " # Check if the tool calls match the expected trajectory exactly\n",
+ " if tool_calls == expected_trajectory:\n",
+ " score = 1\n",
+ " else:\n",
+ " score = 0\n",
+ "\n",
+ " return {\"score\": int(score), \"key\": \"multi_tool_call_in_exact_order\"}\n",
+ "\n",
+ "\n",
+ "def contains_all_tool_calls_in_order(root_run: Run, example: Example) -> dict:\n",
+ " \"\"\"\n",
+ " Check if all expected tools are called in order,\n",
+ " but it allows for other tools to be called in between the expected ones.\n",
+ " \"\"\"\n",
+ " messages = root_run.outputs[\"response\"]\n",
+ " tool_calls = find_tool_calls(messages)\n",
+ "\n",
+ " # Print the tool calls for debugging\n",
+ " print(\"Here are my tool calls:\")\n",
+ " print(tool_calls)\n",
+ "\n",
+ " it = iter(tool_calls)\n",
+ " if all(elem in it for elem in expected_trajectory):\n",
+ " score = 1\n",
+ " else:\n",
+ " score = 0\n",
+ " return {\"score\": int(score), \"key\": \"multi_tool_call_in_order\"}"
+ ]
},
{
"cell_type": "code",
@@ -568,7 +1007,19 @@
"id": "cf02e843-438d-4168-a27a-8f1e0266f8d7",
"metadata": {},
"outputs": [],
- "source": ["experiment_results = evaluate(\n predict_sql_agent_messages,\n data=dataset_name,\n evaluators=[\n contains_all_tool_calls_in_order,\n contains_all_tool_calls_in_order_exact_match,\n ],\n num_repetitions=3,\n experiment_prefix=\"sql-agent-multi-step-tool-calling-trajecory-in-order\",\n metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n)"]
+ "source": [
+ "experiment_results = evaluate(\n",
+ " predict_sql_agent_messages,\n",
+ " data=dataset_name,\n",
+ " evaluators=[\n",
+ " contains_all_tool_calls_in_order,\n",
+ " contains_all_tool_calls_in_order_exact_match,\n",
+ " ],\n",
+ " num_repetitions=3,\n",
+ " experiment_prefix=\"sql-agent-multi-step-tool-calling-trajecory-in-order\",\n",
+ " metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n",
+ ")"
+ ]
},
{
"attachments": {
@@ -609,7 +1060,7 @@
"id": "0681b6e0-196e-440c-ab16-1a530411719e",
"metadata": {},
"outputs": [],
- "source": [""]
+ "source": []
}
],
"metadata": {
diff --git a/examples/tutorials/tnt-llm/tnt-llm.ipynb b/examples/tutorials/tnt-llm/tnt-llm.ipynb
index 3dc2fb50b..e4a55d7f8 100644
--- a/examples/tutorials/tnt-llm/tnt-llm.ipynb
+++ b/examples/tutorials/tnt-llm/tnt-llm.ipynb
@@ -190,7 +190,7 @@
"id": "f1f97ea4-53e5-4f55-8d73-b5b2234a47d9",
"metadata": {},
"outputs": [],
- "source": ["from langgraph.graph import StateGraph, START\n\ngraph = StateGraph(TaxonomyGenerationState)\ngraph.add_node(\"summarize\", map_reduce_chain)\ngraph.add_node(\"get_minibatches\", get_minibatches)\ngraph.add_node(\"generate_taxonomy\", generate_taxonomy)\ngraph.add_node(\"update_taxonomy\", update_taxonomy)\ngraph.add_node(\"review_taxonomy\", review_taxonomy)\n\ngraph.add_edge(\"summarize\", \"get_minibatches\")\ngraph.add_edge(\"get_minibatches\", \"generate_taxonomy\")\ngraph.add_edge(\"generate_taxonomy\", \"update_taxonomy\")\n\n\ndef should_review(state: TaxonomyGenerationState) -> str:\n num_minibatches = len(state[\"minibatches\"])\n num_revisions = len(state[\"clusters\"])\n if num_revisions < num_minibatches:\n return \"update_taxonomy\"\n return \"review_taxonomy\"\n\n\ngraph.add_conditional_edges(\n \"update_taxonomy\",\n should_review,\n # Optional (but required for the diagram to be drawn correctly below)\n {\"update_taxonomy\": \"update_taxonomy\", \"review_taxonomy\": \"review_taxonomy\"},\n)\ngraph.set_finish_point(\"review_taxonomy\")\n\ngraph.add_edge(START, \"summarize\")\napp = graph.compile()"]
+ "source": ["from langgraph.graph import StateGraph, START, END\n\ngraph = StateGraph(TaxonomyGenerationState)\ngraph.add_node(\"summarize\", map_reduce_chain)\ngraph.add_node(\"get_minibatches\", get_minibatches)\ngraph.add_node(\"generate_taxonomy\", generate_taxonomy)\ngraph.add_node(\"update_taxonomy\", update_taxonomy)\ngraph.add_node(\"review_taxonomy\", review_taxonomy)\n\ngraph.add_edge(\"summarize\", \"get_minibatches\")\ngraph.add_edge(\"get_minibatches\", \"generate_taxonomy\")\ngraph.add_edge(\"generate_taxonomy\", \"update_taxonomy\")\n\n\ndef should_review(state: TaxonomyGenerationState) -> str:\n num_minibatches = len(state[\"minibatches\"])\n num_revisions = len(state[\"clusters\"])\n if num_revisions < num_minibatches:\n return \"update_taxonomy\"\n return \"review_taxonomy\"\n\n\ngraph.add_conditional_edges(\n \"update_taxonomy\",\n should_review,\n # Optional (but required for the diagram to be drawn correctly below)\n {\"update_taxonomy\": \"update_taxonomy\", \"review_taxonomy\": \"review_taxonomy\"},\n)\ngraph.add_edge(\"review_taxonomy\", END)\n\ngraph.add_edge(START, \"summarize\")\napp = graph.compile()"]
},
{
"cell_type": "code",
diff --git a/examples/visualization.ipynb b/examples/visualization.ipynb
index 19243b018..43f06aefb 100644
--- a/examples/visualization.ipynb
+++ b/examples/visualization.ipynb
@@ -16,7 +16,10 @@
"id": "32a0e7f4",
"metadata": {},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install -U langgraph"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langgraph"
+ ]
},
{
"cell_type": "markdown",
@@ -34,7 +37,73 @@
"id": "6d604311",
"metadata": {},
"outputs": [],
- "source": ["import random\nfrom typing import Annotated, Literal\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import StateGraph, START\nfrom langgraph.graph.message import add_messages\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]\n\n\nclass MyNode:\n def __init__(self, name: str):\n self.name = name\n\n def __call__(self, state: State):\n return {\"messages\": [(\"assistant\", f\"Called node {self.name}\")]}\n\n\ndef route(state) -> Literal[\"entry_node\", \"__end__\"]:\n if len(state[\"messages\"]) > 10:\n return \"__end__\"\n return \"entry_node\"\n\n\ndef add_fractal_nodes(builder, current_node, level, max_level):\n if level > max_level:\n return\n\n # Number of nodes to create at this level\n num_nodes = random.randint(1, 3) # Adjust randomness as needed\n for i in range(num_nodes):\n nm = [\"A\", \"B\", \"C\"][i]\n node_name = f\"node_{current_node}_{nm}\"\n builder.add_node(node_name, MyNode(node_name))\n builder.add_edge(current_node, node_name)\n\n # Recursively add more nodes\n r = random.random()\n if r > 0.2 and level + 1 < max_level:\n add_fractal_nodes(builder, node_name, level + 1, max_level)\n elif r > 0.05:\n builder.add_conditional_edges(node_name, route, node_name)\n else:\n # End\n builder.add_edge(node_name, \"__end__\")\n\n\ndef build_fractal_graph(max_level: int):\n builder = StateGraph(State)\n entry_point = \"entry_node\"\n builder.add_node(entry_point, MyNode(entry_point))\n builder.add_edge(START, entry_point)\n\n add_fractal_nodes(builder, entry_point, 1, max_level)\n\n # Optional: set a finish point if required\n builder.set_finish_point(entry_point) # or any specific node\n\n return builder.compile()\n\n\napp = build_fractal_graph(3)"]
+ "source": [
+ "import random\n",
+ "from typing import Annotated, Literal\n",
+ "\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "from langgraph.graph import StateGraph, START, END\n",
+ "from langgraph.graph.message import add_messages\n",
+ "\n",
+ "\n",
+ "class State(TypedDict):\n",
+ " messages: Annotated[list, add_messages]\n",
+ "\n",
+ "\n",
+ "class MyNode:\n",
+ " def __init__(self, name: str):\n",
+ " self.name = name\n",
+ "\n",
+ " def __call__(self, state: State):\n",
+ " return {\"messages\": [(\"assistant\", f\"Called node {self.name}\")]}\n",
+ "\n",
+ "\n",
+ "def route(state) -> Literal[\"entry_node\", \"__end__\"]:\n",
+ " if len(state[\"messages\"]) > 10:\n",
+ " return \"__end__\"\n",
+ " return \"entry_node\"\n",
+ "\n",
+ "\n",
+ "def add_fractal_nodes(builder, current_node, level, max_level):\n",
+ " if level > max_level:\n",
+ " return\n",
+ "\n",
+ " # Number of nodes to create at this level\n",
+ " num_nodes = random.randint(1, 3) # Adjust randomness as needed\n",
+ " for i in range(num_nodes):\n",
+ " nm = [\"A\", \"B\", \"C\"][i]\n",
+ " node_name = f\"node_{current_node}_{nm}\"\n",
+ " builder.add_node(node_name, MyNode(node_name))\n",
+ " builder.add_edge(current_node, node_name)\n",
+ "\n",
+ " # Recursively add more nodes\n",
+ " r = random.random()\n",
+ " if r > 0.2 and level + 1 < max_level:\n",
+ " add_fractal_nodes(builder, node_name, level + 1, max_level)\n",
+ " elif r > 0.05:\n",
+ " builder.add_conditional_edges(node_name, route, node_name)\n",
+ " else:\n",
+ " # End\n",
+ " builder.add_edge(node_name, \"__end__\")\n",
+ "\n",
+ "\n",
+ "def build_fractal_graph(max_level: int):\n",
+ " builder = StateGraph(State)\n",
+ " entry_point = \"entry_node\"\n",
+ " builder.add_node(entry_point, MyNode(entry_point))\n",
+ " builder.add_edge(START, entry_point)\n",
+ "\n",
+ " add_fractal_nodes(builder, entry_point, 1, max_level)\n",
+ "\n",
+ " # Optional: set a finish point if required\n",
+ " builder.add_edge(entry_point, END) # or any specific node\n",
+ "\n",
+ " return builder.compile()\n",
+ "\n",
+ "\n",
+ "app = build_fractal_graph(3)"
+ ]
},
{
"cell_type": "markdown",
@@ -96,7 +165,9 @@
]
}
],
- "source": ["app.get_graph().print_ascii()"]
+ "source": [
+ "app.get_graph().print_ascii()"
+ ]
},
{
"cell_type": "markdown",
@@ -155,7 +226,9 @@
]
}
],
- "source": ["print(app.get_graph().draw_mermaid())"]
+ "source": [
+ "print(app.get_graph().draw_mermaid())"
+ ]
},
{
"cell_type": "markdown",
@@ -193,7 +266,18 @@
"output_type": "display_data"
}
],
- "source": ["from IPython.display import Image, display\nfrom langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeColors\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n draw_method=MermaidDrawMethod.API,\n )\n )\n)"]
+ "source": [
+ "from IPython.display import Image, display\n",
+ "from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeColors\n",
+ "\n",
+ "display(\n",
+ " Image(\n",
+ " app.get_graph().draw_mermaid_png(\n",
+ " draw_method=MermaidDrawMethod.API,\n",
+ " )\n",
+ " )\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -219,7 +303,11 @@
}
},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install --quiet pyppeteer\n%pip install --quiet nest_asyncio"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install --quiet pyppeteer\n",
+ "%pip install --quiet nest_asyncio"
+ ]
},
{
"cell_type": "code",
@@ -243,7 +331,25 @@
"output_type": "display_data"
}
],
- "source": ["import nest_asyncio\n\nnest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n\ndisplay(\n Image(\n app.get_graph().draw_mermaid_png(\n curve_style=CurveStyle.LINEAR,\n node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n wrap_label_n_words=9,\n output_file_path=None,\n draw_method=MermaidDrawMethod.PYPPETEER,\n background_color=\"white\",\n padding=10,\n )\n )\n)"]
+ "source": [
+ "import nest_asyncio\n",
+ "\n",
+ "nest_asyncio.apply() # Required for Jupyter Notebook to run async functions\n",
+ "\n",
+ "display(\n",
+ " Image(\n",
+ " app.get_graph().draw_mermaid_png(\n",
+ " curve_style=CurveStyle.LINEAR,\n",
+ " node_colors=NodeColors(start=\"#ffdfba\", end=\"#baffc9\", other=\"#fad7de\"),\n",
+ " wrap_label_n_words=9,\n",
+ " output_file_path=None,\n",
+ " draw_method=MermaidDrawMethod.PYPPETEER,\n",
+ " background_color=\"white\",\n",
+ " padding=10,\n",
+ " )\n",
+ " )\n",
+ ")"
+ ]
},
{
"cell_type": "markdown",
@@ -269,7 +375,10 @@
}
},
"outputs": [],
- "source": ["%%capture --no-stderr\n%pip install pygraphviz"]
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install pygraphviz"
+ ]
},
{
"cell_type": "code",
@@ -293,7 +402,9 @@
"output_type": "display_data"
}
],
- "source": ["display(Image(app.get_graph().draw_png()))"]
+ "source": [
+ "display(Image(app.get_graph().draw_png()))"
+ ]
}
],
"metadata": {