mirror of
https://github.com/langchain-ai/stateful-deepagents.git
synced 2026-07-01 20:14:06 -04:00
242 lines
13 KiB
Plaintext
242 lines
13 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "409d0ca9",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Setup: Agent Config and Tool Definition"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "74fd8f7e",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from typing import Literal\n",
|
||
"import os\n",
|
||
"from tavily import TavilyClient\n",
|
||
"from langchain.agents import create_agent\n",
|
||
"from langchain.agents.middleware.types import AgentMiddleware\n",
|
||
"from langchain_core.messages import HumanMessage, ToolMessage\n",
|
||
"from langchain_core.tools import tool\n",
|
||
"from langchain_openai import ChatOpenAI\n",
|
||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||
"from langgraph.types import Command, interrupt\n",
|
||
"from deepagents.graph import create_deep_agent \n",
|
||
"from deepagents.middleware.subagents import CompiledSubAgent\n",
|
||
"from dotenv import load_dotenv\n",
|
||
"\n",
|
||
"load_dotenv()\n",
|
||
"\n",
|
||
"tavily_client = TavilyClient(os.getenv(\"TAVILY_API_KEY\"))\n",
|
||
"model = ChatOpenAI(model=\"gpt-5-nano\", temperature=0)\n",
|
||
"\n",
|
||
"@tool\n",
|
||
"def get_current_weather(\n",
|
||
" query: str,\n",
|
||
" max_results: int = 5,\n",
|
||
" topic: Literal[\"general\", \"news\", \"finance\"] = \"general\",\n",
|
||
" include_raw_content: bool = False,\n",
|
||
"):\n",
|
||
" \"\"\"\n",
|
||
" A tool to search the web for current weather conditions.\n",
|
||
" Args:\n",
|
||
" query: The query about weather to search for\n",
|
||
" max_results: The maximum number of results to return.\n",
|
||
" topic: The topic of the search.\n",
|
||
" include_raw_content: Whether to include the raw content of the search results.\n",
|
||
" \"\"\"\n",
|
||
" return tavily_client.search(\n",
|
||
" query,\n",
|
||
" max_results=max_results,\n",
|
||
" include_raw_content=include_raw_content,\n",
|
||
" topic=topic,\n",
|
||
" )\n",
|
||
"\n",
|
||
"class WeatherApprovalMiddleware(AgentMiddleware):\n",
|
||
" \"\"\"Pause the weather subagent until a human approves the tool output.\"\"\"\n",
|
||
"\n",
|
||
" def before_model(self, state):\n",
|
||
" messages = state['messages']\n",
|
||
"\n",
|
||
" last_message = messages[-1]\n",
|
||
" if not isinstance(last_message, ToolMessage) or last_message.name != \"get_current_weather\":\n",
|
||
" return None\n",
|
||
"\n",
|
||
" interrupt_payload = {\n",
|
||
" \"prompt\": \"Approve or edit the latest weather lookup result.\",\n",
|
||
" \"tool_call_id\": last_message.tool_call_id,\n",
|
||
" \"tool_name\": last_message.name,\n",
|
||
" \"tool_output\": last_message.content,\n",
|
||
" }\n",
|
||
"\n",
|
||
" decision = interrupt(interrupt_payload)\n",
|
||
"\n",
|
||
" if not isinstance(decision, dict):\n",
|
||
" raise ValueError(\n",
|
||
" \"Resume payload must be a dict with 'status' and optional 'message'.\"\n",
|
||
" )\n",
|
||
"\n",
|
||
" status = decision.get(\"status\")\n",
|
||
" if status not in {\"approved\", \"edit\"}:\n",
|
||
" raise ValueError(\"Resume payload must include status 'approved' or 'edit'.\")\n",
|
||
"\n",
|
||
" review_note = decision.get(\"message\")\n",
|
||
" if status == \"edit\" and not review_note:\n",
|
||
" raise ValueError(\"Edits must include a 'message' detailing the edit to make.\")\n",
|
||
" if not review_note:\n",
|
||
" review_note = \"Weather lookup approved.\"\n",
|
||
"\n",
|
||
" human_msg = HumanMessage(\n",
|
||
" content=f\"[{\"APPROVED\" if status == \"approved\" else \"EDITED\"}] {review_note}\",\n",
|
||
" name=\"tool_reviewer\",\n",
|
||
" additional_kwargs={\n",
|
||
" \"approval_status\": status,\n",
|
||
" \"tool_call_id\": last_message.tool_call_id\n",
|
||
" },\n",
|
||
" )\n",
|
||
" return {\"messages\": [human_msg]}\n",
|
||
"\n",
|
||
"\n",
|
||
"weather_agent_runnable = create_agent(\n",
|
||
" model,\n",
|
||
" system_prompt='You are a weather expert. Use the get_current_weather tool to get the current weather conditions for a given location.',\n",
|
||
" tools=[get_current_weather],\n",
|
||
" middleware=[WeatherApprovalMiddleware()],\n",
|
||
" checkpointer=True, # \n",
|
||
")\n",
|
||
"\n",
|
||
"\n",
|
||
"weather_agent = CompiledSubAgent(\n",
|
||
" name='weather-agent',\n",
|
||
" description='A weather expert that uses the get_current_weather tool to get the current weather conditions for a given location.',\n",
|
||
" runnable=weather_agent_runnable,\n",
|
||
")\n",
|
||
"\n",
|
||
"\n",
|
||
"super_agent_ckpt = InMemorySaver()\n",
|
||
"super_agent = create_deep_agent(\n",
|
||
" model=model,\n",
|
||
" system_prompt='You are a weather expert. You have access to a specialist weather agent that can get the current weather conditions for a given location. Route to the weather agent when you need to get the current weather conditions for a given location.',\n",
|
||
" subagents=[weather_agent],\n",
|
||
" checkpointer=super_agent_ckpt,\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "71db0c2f",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Invoke Agent and Handle Interrupt\n",
|
||
"\n",
|
||
"This cell demonstrates the interrupt workflow:\n",
|
||
"\n",
|
||
"- **Thread Configuration**: Sets up a thread ID for stateful conversation tracking\n",
|
||
"- **Agent Invocation**: Invokes the super agent with a weather query\n",
|
||
"- **Interrupt Handling**: The weather subagent's `before_model` middleware inspects the most recent `ToolMessage` and pauses whenever `get_current_weather` returns so a human can review the tool output before the next LLM call\n",
|
||
"- **Interrupt Inspection**: Extracts and prints the interrupt payload to show what information is available for approval\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "58ab943c",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Super agent interrupt payload: {'prompt': 'Approve or edit the latest weather lookup result.', 'tool_call_id': 'call_GztCZhZ0RB4tqvQbVuKFGnUD', 'tool_name': 'get_current_weather', 'tool_output': '{\"query\": \"San Francisco current weather\", \"follow_up_questions\": null, \"answer\": null, \"images\": [], \"results\": [{\"title\": \"Weather in San Francisco, California, USA\", \"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.775, \\'lon\\': -122.4183, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1762552110, \\'localtime\\': \\'2025-11-07 13:48\\'}, \\'current\\': {\\'last_updated_epoch\\': 1762551900, \\'last_updated\\': \\'2025-11-07 13:45\\', \\'temp_c\\': 19.4, \\'temp_f\\': 66.9, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 4.7, \\'wind_kph\\': 7.6, \\'wind_degree\\': 271, \\'wind_dir\\': \\'W\\', \\'pressure_mb\\': 1020.0, \\'pressure_in\\': 30.12, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 76, \\'cloud\\': 50, \\'feelslike_c\\': 19.4, \\'feelslike_f\\': 66.9, \\'windchill_c\\': 18.9, \\'windchill_f\\': 66.0, \\'heatindex_c\\': 18.9, \\'heatindex_f\\': 66.0, \\'dewpoint_c\\': 16.6, \\'dewpoint_f\\': 61.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 3.4, \\'gust_mph\\': 6.6, \\'gust_kph\\': 10.7}}\", \"score\": 0.8121933, \"raw_content\": null}, {\"url\": \"https://world-weather.info/forecast/usa/san_francisco/november-2025/\", \"title\": \"Weather in San Francisco in November 2025 (California)\", \"content\": \"Detailed ⚡ San Francisco Weather Forecast for November 2025 – day/night 🌡️ temperatures, precipitations – World-Weather.info.\", \"score\": 0.776708, \"raw_content\": null}, {\"url\": \"https://weatherspark.com/h/m/557/2025/7/Historical-Weather-in-July-2025-in-San-Francisco-California-United-States\", \"title\": \"San Francisco July 2025 Historical Weather Data (California, United ...\", \"content\": \"July 2025 Weather History in San Francisco California, United States ; San Francisco Temperature History July 2025 · 50°F · 50°F · 55°F · 55°F ; Hourly Temperature in\", \"score\": 0.7065353, \"raw_content\": null}, {\"url\": \"https://www.cbsnews.com/sanfrancisco/video/friday-morning-first-alert-weather-forecast-with-zoe-mintz-71125/\", \"title\": \"Friday morning First Alert weather forecast with Zoe Mintz - 7/11/25\", \"content\": \"San Francisco 49ers · San Francisco Giants · Golden State Warriors ... Friday morning First Alert weather forecast with Zoe Mintz - 7/11/25.\", \"score\": 0.65412235, \"raw_content\": null}, {\"url\": \"https://world-weather.info/forecast/usa/san_francisco/july-2025/\", \"title\": \"Weather in San Francisco in July 2025 (California)\", \"content\": \"Detailed ⚡ San Francisco Weather Forecast for July 2025 – day/night 🌡️ temperatures, precipitations – World-Weather.info.\", \"score\": 0.6133529, \"raw_content\": null}], \"response_time\": 1.83, \"request_id\": \"2cb97e6b-ffaa-4a4b-9d51-ddca03006961\"}'}\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"\n",
|
||
"thread_config = {'configurable': {'thread_id': '1'}}\n",
|
||
"\n",
|
||
"initial_result = super_agent.invoke(\n",
|
||
" {'messages': [{'role': 'user', 'content': 'What is the current weather in San Francisco?'}]},\n",
|
||
" config=thread_config,\n",
|
||
")\n",
|
||
"interrupt_result = initial_result['__interrupt__'][0]\n",
|
||
"print('Super agent interrupt payload:', interrupt_result.value)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "4d9dbb49",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Resume Agent After Approval\n",
|
||
"\n",
|
||
"This cell demonstrates resuming the agent after approval (or rejection):\n",
|
||
"\n",
|
||
"- **Resume Command**: Creates a `Command` with `resume` data that includes a `status` (\"approved\" or \"rejected\") and an optional `message` (required if rejecting) that will be inserted as a `HumanMessage` after the tool message\n",
|
||
"- **Agent Resume**: Invokes the super agent again with the resume command, continuing from where it was interrupted\n",
|
||
"- **Result Extraction**: Extracts tool messages, the inserted review message, and the final agent response to show the complete workflow result\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"id": "2e2f2b58",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||
"\n",
|
||
"Current weather in San Francisco, CA:\n",
|
||
"- Temperature: 66.9°F (19.4°C)\n",
|
||
"- Conditions: Partly cloudy\n",
|
||
"- Wind: 4.7 mph from the West, gusts up to 6.6 mph\n",
|
||
"- Humidity: 76%\n",
|
||
"- Visibility: 9.0 miles (16.0 km)\n",
|
||
"- Weather alerts: None active\n",
|
||
"- Last updated: 1:45 PM PT\n",
|
||
"\n",
|
||
"Would you like any additional details (dew point, pressure, or hourly forecast)?\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"review_message = {\n",
|
||
" 'status': 'approved',\n",
|
||
" 'message': '',\n",
|
||
"}\n",
|
||
"resume_cmd = Command(resume=review_message)\n",
|
||
"resume_result = super_agent.invoke(resume_cmd, config=thread_config)\n",
|
||
"resume_result['messages'][-1].pretty_print()\n"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": ".venv",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.13.3"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|