\n",
- "
Set up LangSmith for better debugging
\n",
- "
\n",
- " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the docs. \n",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Define model and tools\n",
- "\n",
- "Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n",
- "\n",
- "We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_openai import ChatOpenAI\n",
- "from langchain_core.tools import tool\n",
- "\n",
- "model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
- "\n",
- "\n",
- "@tool\n",
- "def get_weather(location: str):\n",
- " \"\"\"Call to get the weather from a specific location.\"\"\"\n",
- " # This is a placeholder for the actual implementation\n",
- " if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
- " return \"It's sunny!\"\n",
- " elif \"boston\" in location.lower():\n",
- " return \"It's rainy!\"\n",
- " else:\n",
- " return f\"I am not sure what the weather is in {location}\"\n",
- "\n",
- "\n",
- "tools = [get_weather]"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Define tasks\n",
- "\n",
- "Our [tasks](../../concepts/functional_api/#task) are unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n",
- "\n",
- "1. **Call model**: We want to query our chat model with a list of messages.\n",
- "2. **Call tool**: If our model generates tool calls, we want to execute them."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_core.messages import ToolCall, ToolMessage\n",
- "from langgraph.func import entrypoint, task\n",
- "\n",
- "\n",
- "tools_by_name = {tool.name: tool for tool in tools}\n",
- "\n",
- "\n",
- "@task\n",
- "def call_model(messages):\n",
- " \"\"\"Call model with a sequence of messages.\"\"\"\n",
- " response = model.bind_tools(tools).invoke(messages)\n",
- " return response\n",
- "\n",
- "\n",
- "@task\n",
- "def call_tool(tool_call):\n",
- " tool = tools_by_name[tool_call[\"name\"]]\n",
- " observation = tool.invoke(tool_call[\"args\"])\n",
- " return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Define entrypoint\n",
- "\n",
- "To review tool calls before execution, we add a `review_tool_call` function that calls [interrupt](../../concepts/human_in_the_loop/#interrupt). When this function is called, execution will be paused until we issue a command to resume it.\n",
- "\n",
- "Given a tool call, our function will `interrupt` for human review. At that point we can either:\n",
- "\n",
- "- Accept the tool call;\n",
- "- Revise the tool call and continue;\n",
- "- Generate a custom tool message (e.g., instructing the model to re-format its tool call).\n",
- "\n",
- "We will demonstrate these three cases in the [usage examples](#usage) below."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Union\n",
- "\n",
- "\n",
- "def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n",
- " \"\"\"Review a tool call, returning a validated version.\"\"\"\n",
- " human_review = interrupt(\n",
- " {\n",
- " \"question\": \"Is this correct?\",\n",
- " \"tool_call\": tool_call,\n",
- " }\n",
- " )\n",
- " review_action = human_review[\"action\"]\n",
- " review_data = human_review.get(\"data\")\n",
- " if review_action == \"continue\":\n",
- " return tool_call\n",
- " elif review_action == \"update\":\n",
- " updated_tool_call = {**tool_call, **{\"args\": review_data}}\n",
- " return updated_tool_call\n",
- " elif review_action == \"feedback\":\n",
- " return ToolMessage(\n",
- " content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n",
- " )"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can now update our [entrypoint](../../concepts/functional_api/#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human.\n",
- "\n",
- "!!! tip\n",
- "\n",
- " The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [],
- "source": [
- "from langgraph.checkpoint.memory import MemorySaver\n",
- "from langgraph.graph.message import add_messages\n",
- "from langgraph.types import Command, interrupt\n",
- "\n",
- "\n",
- "checkpointer = MemorySaver()\n",
- "\n",
- "\n",
- "@entrypoint(checkpointer=checkpointer)\n",
- "def agent(messages, previous):\n",
- " if previous is not None:\n",
- " messages = add_messages(previous, messages)\n",
- "\n",
- " llm_response = call_model(messages).result()\n",
- " while True:\n",
- " if not llm_response.tool_calls:\n",
- " break\n",
- "\n",
- " # Review tool calls\n",
- " tool_results = []\n",
- " tool_calls = []\n",
- " for i, tool_call in enumerate(llm_response.tool_calls):\n",
- " review = review_tool_call(tool_call)\n",
- " if isinstance(review, ToolMessage):\n",
- " tool_results.append(review)\n",
- " else: # is a validated tool call\n",
- " tool_calls.append(review)\n",
- " if review != tool_call:\n",
- " llm_response.tool_calls[i] = review # update message\n",
- "\n",
- " # Execute remaining tool calls\n",
- " tool_result_futures = [call_tool(tool_call) for tool_call in tool_calls]\n",
- " remaining_tool_results = [fut.result() for fut in tool_result_futures]\n",
- "\n",
- " # Append to message list\n",
- " messages = add_messages(\n",
- " messages,\n",
- " [llm_response, *tool_results, *remaining_tool_results],\n",
- " )\n",
- "\n",
- " # Call model again\n",
- " llm_response = call_model(messages).result()\n",
- "\n",
- " # Generate final response\n",
- " messages = add_messages(messages, llm_response)\n",
- " return entrypoint.final(value=llm_response, save=messages)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Usage\n",
- "\n",
- "Let's demonstrate some scenarios."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "def _print_step(step: dict) -> None:\n",
- " for task_name, result in step.items():\n",
- " if task_name == \"agent\":\n",
- " continue # just stream from tasks\n",
- " print(f\"\\n{task_name}:\")\n",
- " if task_name in (\"__interrupt__\", \"review_tool_call\"):\n",
- " print(result)\n",
- " else:\n",
- " result.pretty_print()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Accept a tool call\n",
- "\n",
- "To accept a tool call, we just indicate in the data we provide in the `Command` that the tool call should pass through."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [],
- "source": [
- "config = {\"configurable\": {\"thread_id\": \"1\"}}"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
- "\n",
- "call_model:\n",
- "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
- "Tool Calls:\n",
- " get_weather (call_Bh5cSwMqCpCxTjx7AjdrQTPd)\n",
- " Call ID: call_Bh5cSwMqCpCxTjx7AjdrQTPd\n",
- " Args:\n",
- " location: San Francisco\n",
- "\n",
- "__interrupt__:\n",
- "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_Bh5cSwMqCpCxTjx7AjdrQTPd', 'type': 'tool_call'}}, resumable=True, ns=['agent:22fcc9cd-3573-b39b-eea7-272a025903e2'], when='during'),)\n"
- ]
- }
- ],
- "source": [
- "user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
- "print(user_message)\n",
- "\n",
- "for step in agent.stream([user_message], config):\n",
- " _print_step(step)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\n",
- "call_tool:\n",
- "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
- "\n",
- "It's sunny!\n",
- "\n",
- "call_model:\n",
- "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
- "\n",
- "The weather in San Francisco is sunny!\n"
- ]
- }
- ],
- "source": [
- "# highlight-next-line\n",
- "human_input = Command(resume={\"action\": \"continue\"})\n",
- "\n",
- "for step in agent.stream(human_input, config):\n",
- " _print_step(step)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Revise a tool call\n",
- "\n",
- "To revise a tool call, we can supply updated arguments."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [],
- "source": [
- "config = {\"configurable\": {\"thread_id\": \"2\"}}"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
- "\n",
- "call_model:\n",
- "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
- "Tool Calls:\n",
- " get_weather (call_b9h8e18FqH0IQm3NMoeYKz6N)\n",
- " Call ID: call_b9h8e18FqH0IQm3NMoeYKz6N\n",
- " Args:\n",
- " location: san francisco\n",
- "\n",
- "__interrupt__:\n",
- "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'san francisco'}, 'id': 'call_b9h8e18FqH0IQm3NMoeYKz6N', 'type': 'tool_call'}}, resumable=True, ns=['agent:9559a81d-5720-dc19-a457-457bac7bdd83'], when='during'),)\n"
- ]
- }
- ],
- "source": [
- "user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
- "print(user_message)\n",
- "\n",
- "for step in agent.stream([user_message], config):\n",
- " _print_step(step)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\n",
- "call_tool:\n",
- "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
- "\n",
- "It's sunny!\n",
- "\n",
- "call_model:\n",
- "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
- "\n",
- "The weather in San Francisco is sunny!\n"
- ]
- }
- ],
- "source": [
- "# highlight-next-line\n",
- "human_input = Command(resume={\"action\": \"update\", \"data\": {\"location\": \"SF, CA\"}})\n",
- "\n",
- "for step in agent.stream(human_input, config):\n",
- " _print_step(step)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The LangSmith traces for this run are particularly informative:\n",
- "\n",
- "- In the trace [before the interrupt](https://smith.langchain.com/public/c8b07579-5cf4-4adb-a849-282163bc9d99/r/b5b128d6-e715-480b-b58d-59e64f724275), we generate a tool call for location `\"San Francisco\"`.\n",
- "- In the trace [after resuming](https://smith.langchain.com/public/b28b92e5-a555-482d-aa4d-c675a19f0eb5/r), we see that the tool call in the message has been updated to `\"SF, CA\"`."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Generate a custom ToolMessage\n",
- "\n",
- "To Generate a custom `ToolMessage`, we supply the content of the message. In this case we will ask the model to reformat its tool call."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "metadata": {},
- "outputs": [],
- "source": [
- "config = {\"configurable\": {\"thread_id\": \"3\"}}"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
- "\n",
- "call_model:\n",
- "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
- "Tool Calls:\n",
- " get_weather (call_VqGjKE7uu8HdWs9XuY1kMV18)\n",
- " Call ID: call_VqGjKE7uu8HdWs9XuY1kMV18\n",
- " Args:\n",
- " location: San Francisco\n",
- "\n",
- "__interrupt__:\n",
- "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_VqGjKE7uu8HdWs9XuY1kMV18', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n"
- ]
- }
- ],
- "source": [
- "user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
- "print(user_message)\n",
- "\n",
- "for step in agent.stream([user_message], config):\n",
- " _print_step(step)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\n",
- "call_model:\n",
- "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
- "Tool Calls:\n",
- " get_weather (call_xoXkK8Cz0zIpvWs78qnXpvYp)\n",
- " Call ID: call_xoXkK8Cz0zIpvWs78qnXpvYp\n",
- " Args:\n",
- " location: San Francisco, CA\n",
- "\n",
- "__interrupt__:\n",
- "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco, CA'}, 'id': 'call_xoXkK8Cz0zIpvWs78qnXpvYp', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n"
- ]
- }
- ],
- "source": [
- "# highlight-next-line\n",
- "human_input = Command(\n",
- " # highlight-next-line\n",
- " resume={\n",
- " # highlight-next-line\n",
- " \"action\": \"feedback\",\n",
- " # highlight-next-line\n",
- " \"data\": \"Please format as